Add native UI window with tao and wry for preview
This commit is contained in:
439
src/main.rs
439
src/main.rs
@@ -131,13 +131,20 @@ fn wrap_in_html(title: &str, content: &str) -> String {
|
|||||||
<title>{}</title>
|
<title>{}</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
|
* {{ box-sizing: border-box; }}
|
||||||
|
html, body {{ margin: 0; padding: 0; height: 100%; }}
|
||||||
body {{
|
body {{
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
max-width: 800px;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
overflow-y: auto;
|
||||||
|
}}
|
||||||
|
.content {{
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
}}
|
}}
|
||||||
pre {{
|
pre {{
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
@@ -214,206 +221,104 @@ fn wrap_in_html(title: &str, content: &str) -> String {
|
|||||||
.mermaid-download:hover {{
|
.mermaid-download:hover {{
|
||||||
background: #0056b3;
|
background: #0056b3;
|
||||||
}}
|
}}
|
||||||
</style>
|
.toolbar {{
|
||||||
</head>
|
position: fixed;
|
||||||
<body>
|
top: 0;
|
||||||
{}
|
left: 0;
|
||||||
<script>
|
right: 0;
|
||||||
async function renderMermaidDiagrams() {{
|
background: #2c3e50;
|
||||||
const mermaidDivs = document.querySelectorAll('.mermaid');
|
color: white;
|
||||||
for (let i = 0; i < mermaidDivs.length; i++) {{
|
padding: 8px 16px;
|
||||||
const div = mermaidDivs[i];
|
display: flex;
|
||||||
const code = div.textContent.trim();
|
align-items: center;
|
||||||
const id = 'mermaid-' + i;
|
gap: 12px;
|
||||||
try {{
|
z-index: 1000;
|
||||||
const {{ svg }} = await mermaid.render(id, code);
|
|
||||||
div.innerHTML = svg + '<button class="mermaid-download" onclick="downloadSvg(this)" data-svg="' + encodeURIComponent(svg) + '">Download SVG</button>';
|
|
||||||
}} catch (err) {{
|
|
||||||
console.error('Mermaid render error:', err);
|
|
||||||
div.innerHTML = '<pre style="text-align:left;color:red;">Error: ' + err.message + '</pre>';
|
|
||||||
}}
|
|
||||||
}}
|
}}
|
||||||
}}
|
.toolbar h1 {{
|
||||||
|
|
||||||
function downloadSvg(btn) {{
|
|
||||||
const svg = decodeURIComponent(btn.getAttribute('data-svg'));
|
|
||||||
const blob = new Blob([svg], {{ type: 'image/svg+xml' }});
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'diagram-' + Date.now() + '.svg';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}}
|
|
||||||
|
|
||||||
mermaid.initialize({{ startOnLoad: false }});
|
|
||||||
renderMermaidDiagrams();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>"#,
|
|
||||||
title, content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wrap_for_pdf(title: &str, content: &str) -> String {
|
|
||||||
format!(
|
|
||||||
r#"<!DOCTYPE html>
|
|
||||||
<html lang="zh-TW">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{}</title>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
||||||
<style>
|
|
||||||
@page {{
|
|
||||||
margin: 20mm;
|
|
||||||
size: A4;
|
|
||||||
}}
|
|
||||||
body {{
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
max-width: none;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
font-size: 16px;
|
||||||
line-height: 1.6;
|
font-weight: 500;
|
||||||
color: #333;
|
|
||||||
font-size: 12pt;
|
|
||||||
}}
|
}}
|
||||||
h1, h2, h3, h4, h5, h6 {{
|
.toolbar button {{
|
||||||
page-break-after: avoid;
|
background: #3498db;
|
||||||
}}
|
color: white;
|
||||||
pre {{
|
border: none;
|
||||||
background: #f4f4f4;
|
padding: 6px 12px;
|
||||||
padding: 12px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow-x: auto;
|
cursor: pointer;
|
||||||
page-break-inside: avoid;
|
font-size: 13px;
|
||||||
font-size: 10pt;
|
|
||||||
}}
|
}}
|
||||||
code {{
|
.toolbar button:hover {{
|
||||||
background: #f4f4f4;
|
background: #2980b9;
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-family: "SF Mono", Monaco, Consolas, monospace;
|
|
||||||
font-size: 10pt;
|
|
||||||
}}
|
|
||||||
pre code {{
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
}}
|
|
||||||
blockquote {{
|
|
||||||
border-left: 3px solid #ddd;
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 12px;
|
|
||||||
color: #666;
|
|
||||||
}}
|
|
||||||
table {{
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}}
|
|
||||||
th, td {{
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 6px;
|
|
||||||
text-align: left;
|
|
||||||
}}
|
|
||||||
th {{
|
|
||||||
background: #f4f4f4;
|
|
||||||
}}
|
|
||||||
img {{
|
|
||||||
max-width: 100%;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}}
|
|
||||||
.mermaid {{
|
|
||||||
background: white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}}
|
|
||||||
.mermaid svg {{
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}}
|
|
||||||
.mermaid-download {{
|
|
||||||
display: none;
|
|
||||||
}}
|
|
||||||
@media print {{
|
|
||||||
body {{
|
|
||||||
-webkit-print-color-adjust: exact;
|
|
||||||
print-color-adjust: exact;
|
|
||||||
}}
|
|
||||||
.mermaid svg {{
|
|
||||||
-webkit-print-color-adjust: exact;
|
|
||||||
print-color-adjust: exact;
|
|
||||||
}}
|
|
||||||
}}
|
}}
|
||||||
|
body {{ padding-top: 50px; }}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="toolbar">
|
||||||
|
<h1>{}</h1>
|
||||||
|
<button onclick="window.print()">Print / Save as PDF</button>
|
||||||
|
<button onclick="downloadAllSvg()">Download All SVGs</button>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
{}
|
{}
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
let svgData = [];
|
||||||
async function renderMermaidDiagrams() {{
|
async function renderMermaidDiagrams() {{
|
||||||
const mermaidDivs = document.querySelectorAll('.mermaid');
|
const mermaidDivs = document.querySelectorAll('.mermaid');
|
||||||
|
svgData = [];
|
||||||
for (let i = 0; i < mermaidDivs.length; i++) {{
|
for (let i = 0; i < mermaidDivs.length; i++) {{
|
||||||
const div = mermaidDivs[i];
|
const div = mermaidDivs[i];
|
||||||
const code = div.textContent.trim();
|
const code = div.textContent.trim();
|
||||||
const id = 'mermaid-' + i;
|
const id = 'mermaid-' + i;
|
||||||
try {{
|
try {{
|
||||||
const {{ svg }} = await mermaid.render(id, code);
|
const {{ svg }} = await mermaid.render(id, code);
|
||||||
div.innerHTML = svg;
|
svgData.push({{ id: i, svg: svg }});
|
||||||
|
div.innerHTML = svg + '<button class="mermaid-download" onclick="downloadSingleSvg(' + i + ')">Download SVG</button>';
|
||||||
}} catch (err) {{
|
}} catch (err) {{
|
||||||
console.error('Mermaid render error:', err);
|
console.error('Mermaid render error:', err);
|
||||||
div.innerHTML = '<pre style="text-align:left;color:red;">Error: ' + err.message + '</pre>';
|
div.innerHTML = '<pre style="text-align:left;color:red;">Error: ' + err.message + '</pre>';
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
setTimeout(function() {{ window.print(); }}, 1500);
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
function downloadSingleSvg(index) {{
|
||||||
|
const data = svgData.find(d => d.id === index);
|
||||||
|
if (data) {{
|
||||||
|
const blob = new Blob([data.svg], {{ type: 'image/svg+xml' }});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'diagram-' + index + '-' + Date.now() + '.svg';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function downloadAllSvg() {{
|
||||||
|
svgData.forEach((data, index) => {{
|
||||||
|
setTimeout(() => {{
|
||||||
|
const blob = new Blob([data.svg], {{ type: 'image/svg+xml' }});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'diagram-' + index + '-' + Date.now() + '.svg';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}}, index * 200);
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
|
||||||
mermaid.initialize({{ startOnLoad: false }});
|
mermaid.initialize({{ startOnLoad: false }});
|
||||||
renderMermaidDiagrams();
|
renderMermaidDiagrams();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>"#,
|
</html>"#,
|
||||||
title, content
|
title, title, content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn export_pdf(file_path: &str, output: Option<String>) -> Result<()> {
|
|
||||||
let path = Path::new(file_path);
|
|
||||||
let content = read_file(path)?;
|
|
||||||
let html = render_markdown(&content);
|
|
||||||
let title = path
|
|
||||||
.file_stem()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("Markdown");
|
|
||||||
|
|
||||||
let pdf_html = wrap_for_pdf(title, &html);
|
|
||||||
|
|
||||||
let out_path = if let Some(o) = output {
|
|
||||||
o
|
|
||||||
} else {
|
|
||||||
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
|
||||||
let out_dir = home.join("docs/pdf");
|
|
||||||
std::fs::create_dir_all(&out_dir)?;
|
|
||||||
out_dir
|
|
||||||
.join(format!("{}.html", title))
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
std::fs::write(&out_path, &pdf_html)
|
|
||||||
.with_context(|| format!("Failed to write to {}", out_path))?;
|
|
||||||
|
|
||||||
println!("PDF export file: {}", out_path);
|
|
||||||
println!("Open this file in browser and use File > Print > Save as PDF");
|
|
||||||
|
|
||||||
open::that(&out_path).context("Failed to open file in browser")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_file(path: &Path) -> Result<String> {
|
fn read_file(path: &Path) -> Result<String> {
|
||||||
std::fs::read_to_string(path)
|
std::fs::read_to_string(path)
|
||||||
.with_context(|| format!("Failed to read file: {}", path.display()))
|
.with_context(|| format!("Failed to read file: {}", path.display()))
|
||||||
@@ -606,27 +511,189 @@ fn list_directory(path: &Path) -> String {
|
|||||||
files.join("\n")
|
files.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview_markdown(file_path: &str, title: &str, _width: u32, _height: u32) -> Result<()> {
|
fn preview_markdown(file_path: &str, title: &str, width: u32, height: u32) -> Result<()> {
|
||||||
|
use tao::event_loop::{EventLoopBuilder, ControlFlow};
|
||||||
|
use tao::window::WindowBuilder;
|
||||||
|
use tao::event::{Event, WindowEvent};
|
||||||
|
use wry::WebViewBuilder;
|
||||||
|
|
||||||
|
let path = Path::new(file_path);
|
||||||
|
let content = read_file(path)?;
|
||||||
|
let html_content = render_markdown(&content);
|
||||||
|
let full_html = wrap_in_html(title, &html_content);
|
||||||
|
|
||||||
|
let event_loop = EventLoopBuilder::new().build();
|
||||||
|
let window = WindowBuilder::new()
|
||||||
|
.with_title(title)
|
||||||
|
.with_inner_size(tao::dpi::LogicalSize::new(width as f64, height as f64))
|
||||||
|
.build(&event_loop)?;
|
||||||
|
|
||||||
|
let _webview = WebViewBuilder::new()
|
||||||
|
.with_html(full_html)
|
||||||
|
.build(&window)?;
|
||||||
|
|
||||||
|
event_loop.run(move |event, _, control_flow| {
|
||||||
|
*control_flow = ControlFlow::Wait;
|
||||||
|
if let Event::WindowEvent { event: WindowEvent::CloseRequested, .. } = event {
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_for_pdf(title: &str, content: &str) -> String {
|
||||||
|
format!(
|
||||||
|
r#"<!DOCTYPE html>
|
||||||
|
<html lang="zh-TW">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{}</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
|
<style>
|
||||||
|
@page {{
|
||||||
|
margin: 20mm;
|
||||||
|
size: A4;
|
||||||
|
}}
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
font-size: 12pt;
|
||||||
|
}}
|
||||||
|
h1, h2, h3, h4, h5, h6 {{
|
||||||
|
page-break-after: avoid;
|
||||||
|
}}
|
||||||
|
pre {{
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
font-size: 10pt;
|
||||||
|
}}
|
||||||
|
code {{
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-family: "SF Mono", Monaco, Consolas, monospace;
|
||||||
|
font-size: 10pt;
|
||||||
|
}}
|
||||||
|
pre code {{
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}}
|
||||||
|
blockquote {{
|
||||||
|
border-left: 3px solid #ddd;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
color: #666;
|
||||||
|
}}
|
||||||
|
table {{
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}}
|
||||||
|
th, td {{
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 6px;
|
||||||
|
text-align: left;
|
||||||
|
}}
|
||||||
|
th {{
|
||||||
|
background: #f4f4f4;
|
||||||
|
}}
|
||||||
|
img {{
|
||||||
|
max-width: 100%;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}}
|
||||||
|
.mermaid {{
|
||||||
|
background: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}}
|
||||||
|
.mermaid svg {{
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}}
|
||||||
|
.mermaid-download {{
|
||||||
|
display: none;
|
||||||
|
}}
|
||||||
|
@media print {{
|
||||||
|
body {{
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}}
|
||||||
|
.mermaid svg {{
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{}
|
||||||
|
<script>
|
||||||
|
async function renderMermaidDiagrams() {{
|
||||||
|
const mermaidDivs = document.querySelectorAll('.mermaid');
|
||||||
|
for (let i = 0; i < mermaidDivs.length; i++) {{
|
||||||
|
const div = mermaidDivs[i];
|
||||||
|
const code = div.textContent.trim();
|
||||||
|
const id = 'mermaid-' + i;
|
||||||
|
try {{
|
||||||
|
const {{ svg }} = await mermaid.render(id, code);
|
||||||
|
div.innerHTML = svg;
|
||||||
|
}} catch (err) {{
|
||||||
|
console.error('Mermaid render error:', err);
|
||||||
|
div.innerHTML = '<pre style="text-align:left;color:red;">Error: ' + err.message + '</pre>';
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
setTimeout(function() {{ window.print(); }}, 1500);
|
||||||
|
}}
|
||||||
|
mermaid.initialize({{ startOnLoad: false }});
|
||||||
|
renderMermaidDiagrams();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"#,
|
||||||
|
title, content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn export_pdf(file_path: &str, output: Option<String>) -> Result<()> {
|
||||||
let path = Path::new(file_path);
|
let path = Path::new(file_path);
|
||||||
let content = read_file(path)?;
|
let content = read_file(path)?;
|
||||||
let html = render_markdown(&content);
|
let html = render_markdown(&content);
|
||||||
let full_html = wrap_in_html(title, &html);
|
let title = path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("Markdown");
|
||||||
|
|
||||||
let temp_dir = std::env::temp_dir();
|
let pdf_html = wrap_for_pdf(title, &html);
|
||||||
let safe_title: String = title
|
|
||||||
.replace(' ', "_")
|
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.')
|
|
||||||
.collect();
|
|
||||||
let temp_file = temp_dir.join(format!("{}.html", safe_title));
|
|
||||||
println!("Temp file: {}", temp_file.display());
|
|
||||||
|
|
||||||
std::fs::write(&temp_file, &full_html)
|
let out_path = if let Some(o) = output {
|
||||||
.with_context(|| format!("Failed to write temp file: {}", temp_file.display()))?;
|
o
|
||||||
|
} else {
|
||||||
|
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
let out_dir = home.join("docs/pdf");
|
||||||
|
std::fs::create_dir_all(&out_dir)?;
|
||||||
|
out_dir
|
||||||
|
.join(format!("{}.html", title))
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
open::that(&temp_file).with_context(|| format!("Failed to open file in browser"))?;
|
std::fs::write(&out_path, &pdf_html)
|
||||||
|
.with_context(|| format!("Failed to write to {}", out_path))?;
|
||||||
|
|
||||||
|
println!("PDF export file: {}", out_path);
|
||||||
|
println!("Open this file in browser and use File > Print > Save as PDF");
|
||||||
|
|
||||||
|
open::that(&out_path).context("Failed to open file in browser")?;
|
||||||
|
|
||||||
println!("Opening preview in browser... (close browser window to exit)");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,7 +744,11 @@ fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Export { file, output, format } => {
|
Commands::Export {
|
||||||
|
file,
|
||||||
|
output,
|
||||||
|
format,
|
||||||
|
} => {
|
||||||
if format == "pdf" {
|
if format == "pdf" {
|
||||||
if let Err(e) = export_pdf(&file, Some(output)) {
|
if let Err(e) = export_pdf(&file, Some(output)) {
|
||||||
eprintln!("Export error: {}", e);
|
eprintln!("Export error: {}", e);
|
||||||
|
|||||||
Reference in New Issue
Block a user