Add i18n support, PyPI proxy, and UI improvements

- Add Russian/English language switcher with cookie persistence
- Add PyPI proxy support with caching (like npm)
- Add height limits to Activity Log and Mount Points tables
- Change Cargo icon to delivery truck
- Replace graphical logo with styled text "NORA"
- Bump version to 0.2.11
This commit is contained in:
2026-01-26 19:31:28 +00:00
parent 0a5f267374
commit 73e7e525a3
9 changed files with 869 additions and 122 deletions

View File

@@ -1,11 +1,12 @@
mod api;
mod components;
pub mod i18n;
mod logo;
mod templates;
use crate::AppState;
use axum::{
extract::{Path, State},
extract::{Path, Query, State},
response::{Html, IntoResponse, Redirect},
routing::get,
Router,
@@ -13,8 +14,33 @@ use axum::{
use std::sync::Arc;
use api::*;
use i18n::Lang;
use templates::*;
#[derive(Debug, serde::Deserialize)]
struct LangQuery {
lang: Option<String>,
}
fn extract_lang(query: &Query<LangQuery>, cookie_header: Option<&str>) -> Lang {
// Priority: query param > cookie > default
if let Some(ref lang) = query.lang {
return Lang::from_str(lang);
}
// Try cookie
if let Some(cookies) = cookie_header {
for part in cookies.split(';') {
let part = part.trim();
if let Some(value) = part.strip_prefix("nora_lang=") {
return Lang::from_str(value);
}
}
}
Lang::default()
}
pub fn routes() -> Router<Arc<AppState>> {
Router::new()
// UI Pages
@@ -40,77 +66,122 @@ pub fn routes() -> Router<Arc<AppState>> {
}
// Dashboard page
async fn dashboard(State(state): State<Arc<AppState>>) -> impl IntoResponse {
async fn dashboard(
State(state): State<Arc<AppState>>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let response = api_dashboard(State(state)).await.0;
Html(render_dashboard(&response))
Html(render_dashboard(&response, lang))
}
// Docker pages
async fn docker_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
async fn docker_list(
State(state): State<Arc<AppState>>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let repos = get_docker_repos(&state.storage).await;
Html(render_registry_list("docker", "Docker Registry", &repos))
Html(render_registry_list("docker", "Docker Registry", &repos, lang))
}
async fn docker_detail(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let detail = get_docker_detail(&state.storage, &name).await;
Html(render_docker_detail(&name, &detail))
Html(render_docker_detail(&name, &detail, lang))
}
// Maven pages
async fn maven_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
async fn maven_list(
State(state): State<Arc<AppState>>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let repos = get_maven_repos(&state.storage).await;
Html(render_registry_list("maven", "Maven Repository", &repos))
Html(render_registry_list("maven", "Maven Repository", &repos, lang))
}
async fn maven_detail(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let detail = get_maven_detail(&state.storage, &path).await;
Html(render_maven_detail(&path, &detail))
Html(render_maven_detail(&path, &detail, lang))
}
// npm pages
async fn npm_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
async fn npm_list(
State(state): State<Arc<AppState>>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let packages = get_npm_packages(&state.storage).await;
Html(render_registry_list("npm", "npm Registry", &packages))
Html(render_registry_list("npm", "npm Registry", &packages, lang))
}
async fn npm_detail(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let detail = get_npm_detail(&state.storage, &name).await;
Html(render_package_detail("npm", &name, &detail))
Html(render_package_detail("npm", &name, &detail, lang))
}
// Cargo pages
async fn cargo_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
async fn cargo_list(
State(state): State<Arc<AppState>>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let crates = get_cargo_crates(&state.storage).await;
Html(render_registry_list("cargo", "Cargo Registry", &crates))
Html(render_registry_list("cargo", "Cargo Registry", &crates, lang))
}
async fn cargo_detail(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let detail = get_cargo_detail(&state.storage, &name).await;
Html(render_package_detail("cargo", &name, &detail))
Html(render_package_detail("cargo", &name, &detail, lang))
}
// PyPI pages
async fn pypi_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
async fn pypi_list(
State(state): State<Arc<AppState>>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let packages = get_pypi_packages(&state.storage).await;
Html(render_registry_list("pypi", "PyPI Repository", &packages))
Html(render_registry_list("pypi", "PyPI Repository", &packages, lang))
}
async fn pypi_detail(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(&Query(query), headers.get("cookie").and_then(|v| v.to_str().ok()));
let detail = get_pypi_detail(&state.storage, &name).await;
Html(render_package_detail("pypi", &name, &detail))
Html(render_package_detail("pypi", &name, &detail, lang))
}