feat: fully offline web interface with embedded Font Awesome

- Replace CDN with local Font Awesome files
- Add favicon handler
- Embed all static assets (CSS, fonts, logo)
- Update README with Font Awesome license attribution
- Bump version to 0.2.3
This commit is contained in:
2026-04-17 18:16:20 +03:00
parent 70d3e6c6cd
commit 6f13679dd9
16 changed files with 8858 additions and 19 deletions

View File

@@ -575,10 +575,20 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
### Font Awesome Free 6.4.0
Веб-интерфейс использует иконки и шрифты **Font Awesome Free**:
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. https://fontawesome.com
## Лицензия ## Лицензия
**AE Anons** [License](LICENSE) **AE Anons** [License](LICENSE)

Binary file not shown.

View File

@@ -575,10 +575,20 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
### Font Awesome Free 6.4.0
Веб-интерфейс использует иконки и шрифты **Font Awesome Free**:
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. https://fontawesome.com
## Лицензия ## Лицензия
**AE Anons** [License](LICENSE) **AE Anons** [License](LICENSE)

View File

@@ -575,10 +575,20 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
### Font Awesome Free 6.4.0
Веб-интерфейс использует иконки и шрифты **Font Awesome Free**:
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. https://fontawesome.com
## Лицензия ## Лицензия
**AE Anons** [License](LICENSE) **AE Anons** [License](LICENSE)

View File

@@ -575,10 +575,20 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
### Font Awesome Free 6.4.0
Веб-интерфейс использует иконки и шрифты **Font Awesome Free**:
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. https://fontawesome.com
## Лицензия ## Лицензия
**AE Anons** [License](LICENSE) **AE Anons** [License](LICENSE)

View File

@@ -575,10 +575,20 @@ ae_anons/
## 🙏 Благодарности ## 🙏 Благодарности
### Nexrender
Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)** Особая благодарность проекту **[Nexrender](https://github.com/inlife/nexrender)**
([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы ([@inlife](https://github.com/inlife) и контрибьюторам) за создание надёжной платформы
для автоматизации After Effects. для автоматизации After Effects.
### Font Awesome Free 6.4.0
Веб-интерфейс использует иконки и шрифты **Font Awesome Free**:
- **Иконки**: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- **Шрифты**: [SIL OFL 1.1](https://scripts.sil.org/OFL)
© 2023 Fonticons, Inc. https://fontawesome.com
## Лицензия ## Лицензия
**AE Anons** [License](LICENSE) **AE Anons** [License](LICENSE)

8788
src/static/fontawesome/all.min.css vendored Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,7 +5,12 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AE Anons - Nexrender Job Manager</title> <title>AE Anons - Nexrender Job Manager</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Favicon -->
<link rel="icon" type="image/png" href="/favicon.ico">
<link rel="apple-touch-icon" href="/favicon.ico">
<link rel="stylesheet" href="/static/fontawesome/all.min.css">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>

View File

@@ -11,6 +11,7 @@ use serde::Serialize;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
#[derive(Clone)] #[derive(Clone)]
@@ -33,43 +34,32 @@ impl JobInfo {
let uid = job.get("uid")?.as_str()?.to_string(); let uid = job.get("uid")?.as_str()?.to_string();
let state = job.get("state")?.as_str()?.to_string(); let state = job.get("state")?.as_str()?.to_string();
// Извлекаем имя выходного файла из postrender actions
let outfile_name = job let outfile_name = job
.get("actions") .get("actions")
.and_then(|a| a.get("postrender")) .and_then(|a| a.get("postrender"))
.and_then(|p| p.as_array()) .and_then(|p| p.as_array())
.and_then(|arr| { .and_then(|arr| {
// Ищем действие copy (в нём финальный путь)
arr.iter() arr.iter()
.find_map(|action| { .find_map(|action| {
// Проверяем, что это действие copy
action action
.get("module") .get("module")
.and_then(|m| m.as_str()) .and_then(|m| m.as_str())
.filter(|&m| m == "@nexrender/action-copy") .filter(|&m| m == "@nexrender/action-copy")
.and_then(|_| { .and_then(|_| action.get("output").and_then(|o| o.as_str()))
// Извлекаем output из copy
action.get("output").and_then(|o| o.as_str())
}) })
})
// Если copy не найдено, пробуем encode
.or_else(|| { .or_else(|| {
arr.iter() arr.iter()
.find_map(|action| action.get("output").and_then(|o| o.as_str())) .find_map(|action| action.get("output").and_then(|o| o.as_str()))
}) })
}) })
.map(|path| { .map(|path| {
// Извлекаем только имя файла из полного пути
std::path::Path::new(path) std::path::Path::new(path)
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or(path) .unwrap_or(path)
.to_string() .to_string()
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| format!("job_{}", uid));
// Если не удалось извлечь, используем UID
format!("job_{}", uid)
});
Some(JobInfo { Some(JobInfo {
uid, uid,
@@ -87,7 +77,6 @@ impl JobInfo {
} }
} }
// web.rs - функция run_web_server
pub async fn run_web_server(config: Config) -> anyhow::Result<()> { pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
let web_port = config.web_port; let web_port = config.web_port;
let state = AppState { let state = AppState {
@@ -97,7 +86,10 @@ pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
let app = Router::new() let app = Router::new()
.route("/", get(index_page)) .route("/", get(index_page))
.route("/favicon.ico", get(favicon))
.route("/static/style.css", get(style_css)) .route("/static/style.css", get(style_css))
.route("/static/fontawesome/all.min.css", get(fontawesome_css))
.nest_service("/static/fontawesome/webfonts", ServeDir::new("src/static/fontawesome/webfonts"))
.route("/assets/logo.png", get(logo_png)) .route("/assets/logo.png", get(logo_png))
.route("/api/jobs", get(list_jobs)) .route("/api/jobs", get(list_jobs))
.route("/api/generate", post(generate_jobs)) .route("/api/generate", post(generate_jobs))
@@ -123,10 +115,17 @@ async fn style_css() -> impl IntoResponse {
([(CONTENT_TYPE, "text/css")], include_str!("static/style.css")) ([(CONTENT_TYPE, "text/css")], include_str!("static/style.css"))
} }
async fn fontawesome_css() -> impl IntoResponse {
([(CONTENT_TYPE, "text/css")], include_str!("static/fontawesome/all.min.css"))
}
async fn logo_png() -> impl IntoResponse { async fn logo_png() -> impl IntoResponse {
([(CONTENT_TYPE, "image/png")], include_bytes!("../assets/logo.png").as_slice()) ([(CONTENT_TYPE, "image/png")], include_bytes!("../assets/logo.png").as_slice())
} }
async fn favicon() -> impl IntoResponse {
([(CONTENT_TYPE, "image/png")], include_bytes!("../assets/logo.png").as_slice())
}
async fn list_jobs(State(state): State<AppState>) -> Result<Json<Vec<JobInfo>>, AppError> { async fn list_jobs(State(state): State<AppState>) -> Result<Json<Vec<JobInfo>>, AppError> {
let jobs_json = fetch_all_jobs(&state.config.nexrender_api_url) let jobs_json = fetch_all_jobs(&state.config.nexrender_api_url)
@@ -195,7 +194,6 @@ async fn stop_all_jobs(State(state): State<AppState>) -> Result<impl IntoRespons
let client = Client::new(); let client = Client::new();
let api_url = &state.config.nexrender_api_url; let api_url = &state.config.nexrender_api_url;
// Получаем все задания
let jobs = fetch_all_jobs(api_url) let jobs = fetch_all_jobs(api_url)
.await .await
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
@@ -205,7 +203,6 @@ async fn stop_all_jobs(State(state): State<AppState>) -> Result<impl IntoRespons
let state = job.get("state").and_then(|s| s.as_str()).unwrap_or(""); let state = job.get("state").and_then(|s| s.as_str()).unwrap_or("");
if state == "queued" || state == "started" || state == "processing" { if state == "queued" || state == "started" || state == "processing" {
if let Some(uid) = job.get("uid").and_then(|u| u.as_str()) { if let Some(uid) = job.get("uid").and_then(|u| u.as_str()) {
// Отправляем DELETE запрос для остановки задания
let _ = client.delete(&format!("{}/{}", api_url, uid)).send().await; let _ = client.delete(&format!("{}/{}", api_url, uid)).send().await;
stopped += 1; stopped += 1;
log::info!("Stopped job: {}", uid); log::info!("Stopped job: {}", uid);
@@ -217,7 +214,6 @@ async fn stop_all_jobs(State(state): State<AppState>) -> Result<impl IntoRespons
Ok((StatusCode::OK, format!("Stopped {} jobs", stopped))) Ok((StatusCode::OK, format!("Stopped {} jobs", stopped)))
} }
// Error handling
struct AppError(StatusCode, String); struct AppError(StatusCode, String);
impl IntoResponse for AppError { impl IntoResponse for AppError {