Initial Programm
This commit is contained in:
commit
748128d4d9
17 changed files with 7059 additions and 0 deletions
7
src-tauri/.gitignore
vendored
Normal file
7
src-tauri/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
5875
src-tauri/Cargo.lock
generated
Normal file
5875
src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
22
src-tauri/Cargo.toml
Normal file
22
src-tauri/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "OH5_QR_Code_Generator"
|
||||
version = "0.1.0"
|
||||
description = "OH5 QR Code Generator"
|
||||
authors = ["jpk aka Shiro"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1"
|
||||
qrcode = { version = "0.14", features = ["image"] }
|
||||
image = "0.25"
|
||||
urlencoding = "2.1"
|
||||
base64 = "0.22"
|
||||
3
src-tauri/build.rs
Normal file
3
src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
10
src-tauri/capabilities/default.json
Normal file
10
src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/icons/icon.ico
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
BIN
src-tauri/icons/icon.png
Normal file
BIN
src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
151
src-tauri/src/main.rs
Normal file
151
src-tauri/src/main.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use qrcode::{QrCode, EcLevel};
|
||||
use image::{DynamicImage, ImageFormat, imageops, Luma};
|
||||
use base64::Engine;
|
||||
use urlencoding::encode;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GeneratePayload {
|
||||
name: String,
|
||||
customer_no: String,
|
||||
machine_from: u32,
|
||||
machine_to: Option<u32>, // None => Single
|
||||
text_body: String,
|
||||
include_icon: bool,
|
||||
icon_bytes: Option<Vec<u8>>, // PNG/JPG bytes (optional)
|
||||
to_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct OutputItem {
|
||||
label: String,
|
||||
filename: String,
|
||||
png_base64: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Output {
|
||||
items: Vec<OutputItem>,
|
||||
}
|
||||
|
||||
fn build_mailto(
|
||||
to: Option<&str>,
|
||||
name: &str,
|
||||
customer_no: &str,
|
||||
machine_id: u32,
|
||||
body: &str,
|
||||
) -> String {
|
||||
// Betreff nur Kundennummer + Fixtext
|
||||
let subject = format!("Kunde: {} hat einen Fehler. QR Code Scan!", customer_no);
|
||||
|
||||
// Body enthält Firmenname, Kundennummer, Maschine + freien Text
|
||||
let full_body = format!(
|
||||
"Kunde: {}\n\nKundennummer: {}\n\nMaschine: {}\n\n{}",
|
||||
name,
|
||||
customer_no,
|
||||
machine_id,
|
||||
body
|
||||
);
|
||||
|
||||
let subject_enc = encode(&subject);
|
||||
let body_enc = encode(&full_body);
|
||||
|
||||
if let Some(addr) = to.filter(|s| !s.is_empty()) {
|
||||
format!("mailto:{}?subject={}&body={}", addr, subject_enc, body_enc)
|
||||
} else {
|
||||
format!("mailto:?subject={}&body={}", subject_enc, body_enc)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn render_qr_png(data: &str, icon_bytes: Option<&[u8]>) -> anyhow::Result<Vec<u8>> {
|
||||
// Hohe Fehlerkorrektur, damit Icon robust ist
|
||||
let code = QrCode::with_error_correction_level(data.as_bytes(), EcLevel::Q)?;
|
||||
let target = 1024u32;
|
||||
|
||||
let qr_luma = code
|
||||
.render::<Luma<u8>>()
|
||||
.min_dimensions(target, target)
|
||||
.quiet_zone(true)
|
||||
.build();
|
||||
|
||||
let mut qr_rgba = DynamicImage::ImageLuma8(qr_luma).to_rgba8();
|
||||
|
||||
if let Some(bytes) = icon_bytes {
|
||||
if let Ok(mut icon) = image::load_from_memory(bytes).map(|i| i.into_rgba8()) {
|
||||
// Icon auf ~20% der QR-Breite skalieren
|
||||
let target_w = (qr_rgba.width() as f32 * 0.20) as u32;
|
||||
let scale = target_w as f32 / icon.width().max(1) as f32;
|
||||
let target_h = (icon.height() as f32 * scale).round().max(1.0) as u32;
|
||||
icon = imageops::resize(&icon, target_w, target_h, imageops::FilterType::Lanczos3);
|
||||
|
||||
let x = (qr_rgba.width() - icon.width()) / 2;
|
||||
let y = (qr_rgba.height() - icon.height()) / 2;
|
||||
imageops::overlay(&mut qr_rgba, &icon, x.into(), y.into());
|
||||
}
|
||||
}
|
||||
|
||||
let dyn_img = DynamicImage::ImageRgba8(qr_rgba);
|
||||
let mut buf = Vec::new();
|
||||
dyn_img.write_to(&mut std::io::Cursor::new(&mut buf), ImageFormat::Png)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn generate_qr(payload: GeneratePayload) -> Result<Output, String> {
|
||||
let mut items = Vec::new();
|
||||
|
||||
let from = payload.machine_from;
|
||||
let to = payload.machine_to.unwrap_or(from);
|
||||
|
||||
if to < from {
|
||||
return Err("Ungültiger Bereich: Bis < Von".into());
|
||||
}
|
||||
|
||||
let icon: Option<&[u8]> = if payload.include_icon {
|
||||
payload.icon_bytes.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for id in from..=to {
|
||||
let to_addr = payload.to_email.as_deref().filter(|s| !s.is_empty()).unwrap_or("service@oh5.de");
|
||||
|
||||
let data = build_mailto(Some(to_addr), &payload.name, &payload.customer_no, id, &payload.text_body);
|
||||
let png = render_qr_png(&data, icon).map_err(|e| format!("QR-Fehler: {e}"))?;
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(png);
|
||||
let label = if from == to {
|
||||
format!("Kunde: {}\nKundennummer: {}\nID: {}", payload.name, payload.customer_no, id)
|
||||
} else {
|
||||
//format!("ID: {}", id)
|
||||
format!("Kunde: {}\nKundennummer: {}\nID: {}", payload.name, payload.customer_no, id)
|
||||
};
|
||||
let filename = format!("qr_{}_{}_{}.png", sanitize(&payload.name), payload.customer_no, id);
|
||||
items.push(OutputItem { label, filename, png_base64: b64 });
|
||||
}
|
||||
|
||||
Ok(Output { items })
|
||||
}
|
||||
|
||||
// kleine Dateinamen-Helferfunktion
|
||||
fn sanitize(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for ch in s.chars() {
|
||||
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
|
||||
out.push(ch);
|
||||
} else if ch.is_whitespace() {
|
||||
out.push('_');
|
||||
}
|
||||
}
|
||||
if out.is_empty() { "kunde".to_string() } else { out }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![generate_qr])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
30
src-tauri/tauri.conf.json
Normal file
30
src-tauri/tauri.conf.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OH5 QR Code Generator",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.jpk.oh5-qr-generator",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "OH5 QR Code Generator",
|
||||
"width": 1280,
|
||||
"height": 700
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/icon.png",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue