Added and implemented English translation
This commit is contained in:
		
							parent
							
								
									1b75c799e5
								
							
						
					
					
						commit
						49f326b5ae
					
				| @ -2,8 +2,6 @@ | ||||
| 
 | ||||
| CatNote is a minimalist web application that lets you generate Markdown-based receipts or notes, render them as PNGs, and send them directly to an MXW01 Cat Thermal Printer. Ideal for making quick, beautiful, and portable printed notes using Markdown, with optional image upload. | ||||
| 
 | ||||
| **Note:** The default UI is currently *only available in Spanish*, as this project began for my own use. Support for both English and Spanish is planned for a future release. | ||||
| 
 | ||||
| ## Features | ||||
| 
 | ||||
| * Enter Markdown and preview output. | ||||
|  | ||||
							
								
								
									
										582
									
								
								app.py
									
									
									
									
									
								
							
							
						
						
									
										582
									
								
								app.py
									
									
									
									
									
								
							| @ -3,11 +3,12 @@ import tempfile | ||||
| import subprocess | ||||
| import requests | ||||
| import base64 | ||||
| from flask import Flask, render_template_string, request, send_from_directory, session | ||||
| from flask import Flask, render_template, request, send_from_directory, session | ||||
| from PIL import Image, ImageDraw, ImageFont | ||||
| import os | ||||
| import re | ||||
| import argparse | ||||
| import json | ||||
| 
 | ||||
| app = Flask(__name__) | ||||
| app.secret_key = 'TpfxYMxUJxJMtCCTraBSg1rbd6NLz38JTmIfpBLsotcI47EqXU' | ||||
| @ -24,562 +25,8 @@ BANNER_FONT_SIZE = 300 | ||||
| IMAGE_WIDTH = 384 | ||||
| BULLET_CHAR = "• " | ||||
| 
 | ||||
| DITHERING_MODES = { | ||||
|   "Sin dithering": "none", | ||||
|   "Floyd-Steinberg": "floyd", | ||||
|   "Bayer 2x2": "bayer2x2", | ||||
|   "Bayer 4x4": "bayer4x4", | ||||
|   "Bayer 8x8": "bayer8x8", | ||||
|   "Bayer 16x16": "bayer16x16", | ||||
|   "Atkinson": "atkinson", | ||||
|   "Jarvis-Judice-Ninke": "jjn" | ||||
| } | ||||
| 
 | ||||
| HTML_FORM = ''' | ||||
| <!doctype html> | ||||
| <html> | ||||
| <head> | ||||
|   <title>CatNote</title> | ||||
|   <link rel="manifest" href="/static/manifest.json"> | ||||
|   <style> | ||||
|     body { | ||||
|       font-family: sans-serif; | ||||
|       background: #181c1f; | ||||
|       color: #ddd; | ||||
|       min-height: 100vh; | ||||
|       margin: 0; | ||||
|     } | ||||
|     .centered-flex { | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       align-items: flex-start; | ||||
|       height: 100vh; | ||||
|       gap: 2.6em; | ||||
|       padding-top: 2em; | ||||
|     } | ||||
|     .form-card { | ||||
|       background: #22282c; | ||||
|       padding: 2em 2em 1em 2em; | ||||
|       border-radius: 1.5em; | ||||
|       box-shadow: 0 0 12px 0 #000a; | ||||
|       min-width: 410px; | ||||
|       max-width: 450px; | ||||
|       margin-right: 0; | ||||
|       box-sizing: border-box; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|     } | ||||
|     .markdown-ref { | ||||
|       background: #22282c; | ||||
|       border-radius: 1.2em; | ||||
|       box-shadow: 0 0 12px 0 #000a; | ||||
|       padding: 2em 2em 1.5em 2em; | ||||
|       color: #b6c8e0; | ||||
|       font-size: 1.08rem; | ||||
|       font-family: inherit; | ||||
|       box-sizing: border-box; | ||||
|       width: 100%; | ||||
|       max-width: 410px; | ||||
|     } | ||||
|     .markdown-ref h4 { | ||||
|       margin: 0 0 0.4em 0; | ||||
|       color: #88e0ff; | ||||
|       font-size: 1.12rem; | ||||
|       font-weight: 700; | ||||
|       letter-spacing: 0.01em; | ||||
|     } | ||||
|     .markdown-ref ul { | ||||
|       margin: 0.6em 0 0.3em 0.1em; | ||||
|       padding-left: 1.1em; | ||||
|     } | ||||
|     .markdown-ref li { | ||||
|       margin-bottom: 0.36em; | ||||
|     } | ||||
|     .markdown-ref code { | ||||
|       background: #181c1f; | ||||
|       border-radius: 0.5em; | ||||
|       padding: 0.12em 0.46em; | ||||
|       color: #aef6cb; | ||||
|       font-size: 0.98rem; | ||||
|       margin-left: 0.18em; | ||||
|     } | ||||
|     textarea { | ||||
|       width: 100%; | ||||
|       height: 14em; | ||||
|       padding: 0.75em; | ||||
|       border-radius: 0.7em; | ||||
|       border: 1.5px solid #444; | ||||
|       background: #181c1f; | ||||
|       color: #eee; | ||||
|       font-size: 1.06rem; | ||||
|       margin-bottom: 1em; | ||||
|       resize: vertical; | ||||
|       font-family: 'DejaVu Sans Mono', 'Fira Mono', 'monospace'; | ||||
|       box-sizing: border-box; | ||||
|       margin-right: 0; | ||||
|       min-width: 0; | ||||
|     } | ||||
|     .buttons { | ||||
|       display: flex; | ||||
|       gap: 1em; | ||||
|       justify-content: center; | ||||
|     } | ||||
|     button[type=submit] { | ||||
|       background: linear-gradient(90deg, #8ee3c1, #35a7ff); | ||||
|       color: #222; | ||||
|       font-weight: bold; | ||||
|       font-size: 1.12rem; | ||||
|       border: none; | ||||
|       border-radius: 0.7em; | ||||
|       padding: 0.5em 1.6em; | ||||
|       margin-top: 0.1em; | ||||
|       box-shadow: 0 2px 12px #1116; | ||||
|       cursor: pointer; | ||||
|       transition: filter 0.2s, box-shadow 0.2s; | ||||
|       filter: brightness(1); | ||||
|     } | ||||
|     button[type=submit]:hover { | ||||
|       filter: brightness(1.12); | ||||
|       box-shadow: 0 4px 18px #2229; | ||||
|     } | ||||
|     button[type=submit][name=print] { | ||||
|       background: linear-gradient(90deg, #ffeb3b, #ff9100); | ||||
|       color: #181c1f; | ||||
|     } | ||||
|     .preview-card { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       background: #23282d; | ||||
|       padding: 2em 1.4em 1em 1.4em; | ||||
|       border-radius: 1.5em; | ||||
|       box-shadow: 0 0 12px 0 #0007; | ||||
|       min-width: 400px; | ||||
|       text-align: center; | ||||
|       box-sizing: border-box; | ||||
|       margin-left: 0; | ||||
|       align-items: center; | ||||
|     } | ||||
|     img { | ||||
|       margin-top: 1em; | ||||
|       max-width: 100%; | ||||
|       background: #fff; | ||||
|       box-shadow: 0 0 8px 1px #111a; | ||||
|     } | ||||
|     .status-msg { | ||||
|       margin: 0.6em 0 0.1em 0; | ||||
|       font-size: 1.12rem; | ||||
|       border-radius: 0.7em; | ||||
|       background: #294b3a; | ||||
|       color: #9fffc6; | ||||
|       padding: 0.55em 1.2em; | ||||
|       font-weight: 600; | ||||
|       box-shadow: 0 0 6px #183a24a0; | ||||
|       display: inline-block; | ||||
|       border-left: 4px solid #4ffab0; | ||||
|     } | ||||
|     .status-err { | ||||
|       background: #48202c; | ||||
|       color: #ffdbe4; | ||||
|       border-left: 4px solid #ff6384; | ||||
|       box-shadow: 0 0 8px #22000690; | ||||
|     } | ||||
|     label { | ||||
|       font-size: 1em; | ||||
|       padding: 0 1em 0 1em; | ||||
|     } | ||||
|     input[type=file] { | ||||
|       margin: 0.5em 1em 1.5em 1em; | ||||
|     } | ||||
|     input[type=file]::file-selector-button { | ||||
|       background: linear-gradient(90deg, #8ee3c1, #35a7ff); | ||||
|       color: #222; | ||||
|       font-weight: bold; | ||||
|       border: none; | ||||
|       padding: 0.5em 1.2em; | ||||
|       border-radius: 0.7em; | ||||
|       box-shadow: 0 2px 12px #1116; | ||||
|       cursor: pointer; | ||||
|       transition: filter 0.2s, box-shadow 0.2s; | ||||
|       margin-right: 0.5em; | ||||
|     } | ||||
|     input[type=file]::file-selector-button:hover { | ||||
|       filter: brightness(1.12); | ||||
|       box-shadow: 0 4px 18px #2229; | ||||
|     } | ||||
|     .print-frame { | ||||
|       position: relative; | ||||
|       padding: 10px 0 10px 0; | ||||
|       margin-top: 1em; | ||||
|       overflow: hidden; | ||||
|       max-width: 424px; | ||||
|     } | ||||
|     .print-frame::before, | ||||
|     .print-frame::after { | ||||
|       content: ""; | ||||
|       position: absolute; | ||||
|       left: 0; | ||||
|       width: 100%; | ||||
|       height: 10px; | ||||
|       background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDQuMjMzMyA0LjIzMzMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIHRyYW5zZm9ybT0ibWF0cml4KC4wMzYxMzUgMCAwIC4wNDE3MjUgMS4xNTY4IDEuMTI4OSkiIGQ9Im04NS4xNDEgNzQuNDAzLTExNy4xNS0xZS02IDU4LjU3Ny0xMDEuNDZ6IiBmaWxsPSIjZmZmIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBzdHJva2Utb3BhY2l0eT0iLjk3NjQ3IiBzdHJva2Utd2lkdGg9Ii4yNjQ1OCIvPgo8L3N2Zz4K') repeat-x left; | ||||
|       background-size: 10px 384px; | ||||
|       z-index: 1; | ||||
|     } | ||||
|     .print-frame::before { | ||||
|       top: 1px; | ||||
|     } | ||||
|     .print-frame::after { | ||||
|       bottom: 1px; | ||||
|       transform: rotate(180deg); | ||||
|     } | ||||
|     .print-frame img { | ||||
|       background: #fff; | ||||
|       padding: 20px; | ||||
|       display: block; | ||||
|       margin: 0 auto; | ||||
|       box-shadow: none; | ||||
|       position: relative; | ||||
|       width: 100%; | ||||
|       max-width: 384px; | ||||
|       z-index: 2; | ||||
|     } | ||||
|     textarea { | ||||
|       font-size: 1.16em !important; | ||||
|       padding: 1em; | ||||
|       min-height: 12em; | ||||
|     } | ||||
|     .options { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       align-items: left; | ||||
|       justify-content: space-between; | ||||
|       margin-bottom: 1.2em; | ||||
|     } | ||||
|     select { | ||||
|       background: #181c1f; | ||||
|       text-align: center; | ||||
|       text-align-last: center; | ||||
|       border: 1.5px solid #444; | ||||
|       color: #eee; | ||||
|       border-radius: 0.7em; | ||||
|       margin: 1em; | ||||
|       padding: 0.5em 1em; | ||||
|     } | ||||
|     select option { | ||||
|       text-align: left; | ||||
|     } | ||||
|     input[type=number] { | ||||
|       background: #181c1f; | ||||
|       text-align: center; | ||||
|       border: 1.5px solid #444; | ||||
|       color: #eee; | ||||
|       border-radius: 0.7em; | ||||
|       margin: 1em; | ||||
|       padding: 0.5em 1em; | ||||
|     } | ||||
|     input[type=number]::-webkit-inner-spin-button, | ||||
|     input[type=number]::-webkit-outer-spin-button { | ||||
|       -webkit-appearance: none; | ||||
|       margin: 0; | ||||
|     } | ||||
|     .options-dithering, | ||||
|     .options-intensity, | ||||
|     .options-rotation, | ||||
|     .options-printmode { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|     } | ||||
|     #loading-box { | ||||
|       display: none; | ||||
|       flex-direction: column; | ||||
|       text-align: center; | ||||
|       box-sizing: border-box; | ||||
|       width: 100%; | ||||
|       padding: 1em; | ||||
|     } | ||||
|     #printing-text { | ||||
|       color: #eee; | ||||
|       font-size: 1em; | ||||
|       font-weight: bold; | ||||
|       margin-bottom: 1em; | ||||
|     } | ||||
|     .progress-bar { | ||||
|       position: relative; | ||||
|       width: 100%; | ||||
|       height: 18px; | ||||
|       background: #181c1f; | ||||
|       border: 1.5px solid #444; | ||||
|       border-radius: 12px; | ||||
|       overflow: hidden; | ||||
|       box-shadow: 0 2px 12px #1116; | ||||
|     } | ||||
|     .progress-bar-inner { | ||||
|       width: 40%; | ||||
|       height: 100%; | ||||
|       background: linear-gradient(90deg, #8ee3c1, #35a7ff); | ||||
|       border-radius: 12px; | ||||
|       position: absolute; | ||||
|       left: -40%; | ||||
|       animation: indeterminate-bar 1.2s infinite | ||||
|     } | ||||
|     @keyframes indeterminate-bar { | ||||
|       0% { left: 0%; } | ||||
|       50% { left: calc(100% - 40%); } | ||||
|       100% { left: 0%; } | ||||
|     } | ||||
|     /* this next bit is a mess but it works */ | ||||
|     .options-bannermode { | ||||
|       display: flex; | ||||
|       align-items: left; | ||||
|       justify-content: space-between; | ||||
|       margin-left: 1em; | ||||
|       margin-right: 1em; | ||||
|     } | ||||
|     .options-bannermode label { | ||||
|       font-size: 1em; | ||||
|       color: #eee; | ||||
|       margin-bottom: 1em; | ||||
|       display: block; | ||||
|     } | ||||
|     .options-bannermode-inner { | ||||
|       display: block; | ||||
|       align-items: center; | ||||
|       position: relative; | ||||
|       padding-left: 35px; | ||||
|       margin-bottom: 12px; | ||||
|       cursor: pointer; | ||||
|       font-size: 1em; | ||||
|       -webkit-user-select: none; | ||||
|       -moz-user-select: none; | ||||
|       -ms-user-select: none; | ||||
|       user-select: none; | ||||
|     } | ||||
| 
 | ||||
|     /* Hide the browser's default checkbox */ | ||||
|     .options-bannermode-inner input { | ||||
|       position: absolute; | ||||
|       opacity: 0; | ||||
|       cursor: pointer; | ||||
|       height: 0; | ||||
|       width: 0; | ||||
|     } | ||||
| 
 | ||||
|     /* Create a custom checkbox */ | ||||
|     .checkmark { | ||||
|       position: absolute; | ||||
|       top: -25%; | ||||
|       left: 0; | ||||
|       height: 25px; | ||||
|       width: 25px; | ||||
|       background-color: #181c1f; | ||||
|       border-radius: 0.35em; | ||||
|       border: 1.5px solid #444; | ||||
|     } | ||||
| 
 | ||||
|     /* On mouse-over, add a grey background color */ | ||||
|     .options-bannermode-inner:hover input ~ .checkmark { | ||||
|       filter: brightness(1.12); | ||||
|     } | ||||
| 
 | ||||
|     /* When the checkbox is checked, add a blue background */ | ||||
|     .options-bannermode-inner input:checked ~ .checkmark { | ||||
|       background: linear-gradient(135deg, #8ee3c1, #35a7ff); | ||||
|     } | ||||
| 
 | ||||
|     /* Create the checkmark/indicator (hidden when not checked) */ | ||||
|     .checkmark:after { | ||||
|       content: ""; | ||||
|       position: absolute; | ||||
|       display: none; | ||||
|     } | ||||
| 
 | ||||
|     /* Show the checkmark when checked */ | ||||
|     .options-bannermode-inner input:checked ~ .checkmark:after { | ||||
|       display: block; | ||||
|     } | ||||
| 
 | ||||
|     /* Style the checkmark/indicator */ | ||||
|     .options-bannermode-inner .checkmark:after { | ||||
|       left: 9px; | ||||
|       top: 5px; | ||||
|       width: 5px; | ||||
|       height: 10px; | ||||
|       border: solid white; | ||||
|       border-width: 0 3px 3px 0; | ||||
|       -webkit-transform: rotate(45deg); | ||||
|       -ms-transform: rotate(45deg); | ||||
|       transform: rotate(45deg); | ||||
|     } | ||||
|     @media (max-width: 1000px) { | ||||
|       body { | ||||
|         font-size: 1.28em !important; | ||||
|       } | ||||
|       h2, h3, h4 { | ||||
|         font-size: 2em !important; | ||||
|       } | ||||
|       .centered-flex { | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         height: auto; | ||||
|         gap: 1.6em; | ||||
|       } | ||||
|       .form-card, | ||||
|       .preview-card, | ||||
|       .markdown-ref { | ||||
|         font-size: 1.18em !important; | ||||
|         min-width: auto; | ||||
|         max-width: 90vw; | ||||
|         width: 100%; | ||||
|         margin: 0.8em 0; | ||||
|       } | ||||
|       textarea { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: 1em; | ||||
|         min-height: 12em; | ||||
|       } | ||||
|       .status-msg { | ||||
|         font-size: 1.25em !important; | ||||
|         padding: 0.8em 1.6em; | ||||
|       } | ||||
|       button[type=submit] { | ||||
|         font-size: 1.3em !important; | ||||
|         padding: 0.9em 2.2em; | ||||
|       } | ||||
|       input[type=file] { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: inherit 2.2em; | ||||
|       } | ||||
|       label { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: inherit 2.2em; | ||||
|       } | ||||
|       select { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: 0.6em 1.2em; | ||||
|       } | ||||
|       input[type=number] { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: 0.6em 1.2em; | ||||
|       } | ||||
|       /* i'm so sorry, i'm not bothered to find out why or how this works */ | ||||
|       .options-bannermode-inner .checkmark { | ||||
|         top: -10%; | ||||
|       } | ||||
|       .options-bannermode-inner .checkmark::after { | ||||
|         left: 13px; | ||||
|         top: 7px; | ||||
|         transform: rotate(45deg) scale(1.66); | ||||
|       } | ||||
|       .checkmark { | ||||
|         width: 1.2em; | ||||
|         height: 1.2em; | ||||
|       } | ||||
|       .options-bannermode-inner { | ||||
|         padding-left: 55px; | ||||
|       } | ||||
|   </style> | ||||
| </head> | ||||
| <body> | ||||
|   <div class="centered-flex"> | ||||
|     <div class="form-card"> | ||||
|       <center> | ||||
|         <h2 style="margin-top:0.2em;">😺 CatNote 🖨️</h2> | ||||
|       </center> | ||||
|       <form method="post" enctype="multipart/form-data"> | ||||
|         <textarea name="md" placeholder="Ingrese Markdown aquí...">{{ default_md }}</textarea><br> | ||||
|         <label for="userimg" style="display: inline-block; margin-bottom: 0.5em;">Subir imagen (opcional):</label><br> | ||||
|         <input type="file" name="userimg" accept="image/png, image/jpeg"><br> | ||||
|         <div class="options"> | ||||
|         <div class="options-bannermode"> | ||||
|           <label class="options-bannermode-inner"> | ||||
|             <input type="checkbox" name="bannermode" value="1" {% if current_bannermode %}checked{% endif %}> | ||||
|             <span class="checkmark"></span> | ||||
|             Modo Banner (vertical) | ||||
|           </label> | ||||
|           </div> | ||||
|           <div class="options-dithering"> | ||||
|             <label for="dithering">Dithering:</label> | ||||
|             <select name="dithering"> | ||||
|               {% for label, mode in dithering_modes.items() %} | ||||
|                 <option value="{{ mode }}" {% if mode == current_dithering %}selected="selected"{% endif %}>{{ label }}</option> | ||||
|               {% endfor %} | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="options-intensity"> | ||||
|             <label for="intensity">Intensidad:</label> | ||||
|             <input type="number" name="intensity" value="85" min="0" max="100" step="1"> | ||||
|           </div> | ||||
|           <div class="options-rotation"> | ||||
|             <label for="rotation">Rotar imagen:</label> | ||||
|             <select name="rotation"> | ||||
|               <option value="0" {% if 0 == current_rotation %}selected="selected"{% endif %}>0°</option> | ||||
|               <option value="90" {% if 90 == current_rotation %}selected="selected"{% endif %}>90°</option> | ||||
|               <option value="180" {% if 180 == current_rotation %}selected="selected"{% endif %}>180°</option> | ||||
|               <option value="270" {% if 270 == current_rotation %}selected="selected"{% endif %}>270°</option> | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="options-printmode"> | ||||
|             <label for="printmode">Modo de impresión:</label> | ||||
|             <select name="printmode"> | ||||
|               <option value="1bpp" {% if "1bpp" == current_printmode %}selected="selected"{% endif %}>1-bit</option> | ||||
|               <option value="4bpp" {% if "4bpp" == current_printmode %}selected="selected"{% endif %}>4-bit</option> | ||||
|             </select> | ||||
|           </div> | ||||
|           <div id="loading-box"> | ||||
|             <div id="printing-text">Imprimiendo...</div> | ||||
|             <div class="progress-bar"> | ||||
|               <div class="progress-bar-inner"></div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="buttons"> | ||||
|           <button type="submit" name="generate">📷 Generar</button> | ||||
|           <button type="submit" name="print">🖨️ Imprimir</button> | ||||
|         </div> | ||||
|       </form> | ||||
|       {% if printed %} | ||||
|         <div class="status-msg">✅ Enviado a impresora</div> | ||||
|       {% endif %} | ||||
|       {% if error %} | ||||
|         <div class="status-msg status-err">⚠️ {{ error }}</div> | ||||
|       {% endif %} | ||||
|     </div> | ||||
|     <div class="preview-card"> | ||||
|       {% if img %} | ||||
|         <h2 style="margin-top:0.1em; margin-bottom:0;">Vista previa</h2> | ||||
|         <div class="print-frame"> | ||||
|           <img src="data:image/png;base64,{{ img }}"> | ||||
|         </div> | ||||
|       {% else %} | ||||
|         <p style="opacity:.6;">Su vista previa aparecerá aquí</p> | ||||
|       {% endif %} | ||||
|     </div> | ||||
|     <div class="markdown-ref"> | ||||
|       <h4>Referencia rápida de Markdown</h4> | ||||
|       <ul> | ||||
|         <li><b>Negrita:</b> <code>**texto**</code></li> | ||||
|         <li><b>Cursiva:</b> <code>*texto*</code></li> | ||||
|         <li><b>Negrita y cursiva:</b> <code>***texto***</code></li> | ||||
|         <li><b>Encabezado grande:</b> <code># Título</code></li> | ||||
|         <li><b>Encabezado mediano:</b> <code>## Título</code></li> | ||||
|         <li><b>Encabezado chico:</b> <code>### Título</code></li> | ||||
|         <li><b>Lista con viñetas:</b> <code>- Elemento</code></li> | ||||
|         <li><b>Lista numerada:</b> <code>1. Elemento</code></li> | ||||
|         <li><b>Imágen:</b> <code></code></li> | ||||
|         <li><b>Salto de línea:</b> Deje una línea vacía</li> | ||||
|         <li><b>Imagen subida:</b> <code>!(img)</code> (usa la imagen cargada abajo)</li> | ||||
|       </ul> | ||||
|     </div> | ||||
|   </div> | ||||
|   <script> | ||||
|     document.querySelector('form').addEventListener('submit', function(e) { | ||||
|       if (document.activeElement && document.activeElement.name === "print") { | ||||
|         document.getElementById('loading-box').style.display = 'flex'; | ||||
|       } | ||||
|     }); | ||||
|   </script> | ||||
| </body> | ||||
| </html> | ||||
| ''' | ||||
| with open('static/translations.json', 'r', encoding='utf-8') as f: | ||||
|     TRANSLATIONS = json.load(f) | ||||
| 
 | ||||
| def remove_uploaded_img(): | ||||
|     path = session.pop('uploaded_img_path', None) | ||||
| @ -906,6 +353,20 @@ def index(): | ||||
|     md = "" | ||||
|     printed = False | ||||
|     error = None | ||||
|     lang = request.args.get('lang') or session.get('lang', 'en') | ||||
|     session['lang'] = lang | ||||
|     t = TRANSLATIONS.get(lang, TRANSLATIONS['en']) | ||||
|     dithering_modes = { | ||||
|       t['no_dithering']: "none", | ||||
|       "Floyd-Steinberg": "floyd", | ||||
|       "Bayer 2x2": "bayer2x2", | ||||
|       "Bayer 4x4": "bayer4x4", | ||||
|       "Bayer 8x8": "bayer8x8", | ||||
|       "Bayer 16x16": "bayer16x16", | ||||
|       "Atkinson": "atkinson", | ||||
|       "Jarvis-Judice-Ninke": "jjn" | ||||
|     } | ||||
| 
 | ||||
|     if request.method == "POST": | ||||
|         userimg = request.files.get("userimg") | ||||
|         uploaded_img_bytes = None | ||||
| @ -977,8 +438,8 @@ def index(): | ||||
|                         printed = True | ||||
|             except Exception as e: | ||||
|                 error = f"Failed to print: {e}" | ||||
|     return render_template_string(HTML_FORM, dithering_modes=DITHERING_MODES, img=img_data, default_md=md, | ||||
|         printed=printed, error=error, current_dithering=session.get('dithering', 'floyd'), current_rotation=session.get('rotation', 0), current_printmode=session.get('printmode', '1bpp'), current_bannermode=session.get('bannermode', False)) | ||||
|     return render_template("index.html", dithering_modes=dithering_modes, img=img_data, default_md=md, | ||||
|         printed=printed, error=error, current_dithering=session.get('dithering', 'floyd'), current_rotation=session.get('rotation', 0), current_printmode=session.get('printmode', '1bpp'), current_bannermode=session.get('bannermode', False), t=t, lang=lang) | ||||
| 
 | ||||
| @app.route('/manifest.json') | ||||
| def manifest(): | ||||
| @ -996,5 +457,4 @@ if __name__ == "__main__": | ||||
|     parser = argparse.ArgumentParser(description='CatNote: Server for Markdown to MXW01 Cat Printer') | ||||
|     parser.add_argument('-p', '--port', type=int, default=5000, help='Port to run the server on (default: 5000)') | ||||
|     args = parser.parse_args() | ||||
|     app.run(host='0.0.0.0', port=args.port) | ||||
|    | ||||
|     app.run(host='0.0.0.0', port=args.port, debug=True) | ||||
| @ -2,3 +2,4 @@ Flask | ||||
| markdown | ||||
| Pillow | ||||
| requests | ||||
| json | ||||
							
								
								
									
										74
									
								
								static/translations.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								static/translations.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | ||||
| { | ||||
|     "en": { | ||||
|         "catnote": "CatNote", | ||||
|         "enter_markdown_here": "Enter Markdown here...", | ||||
|         "upload_image_optional": "Upload image (optional):", | ||||
|         "banner_mode_vertical": "Banner Mode (vertical)", | ||||
|         "dithering": "Dithering:", | ||||
|         "intensity": "Intensity:", | ||||
|         "rotate_image": "Rotate image:", | ||||
|         "print_mode": "Print mode:", | ||||
|         "printing": "Printing...", | ||||
|         "sent_to_printer": "Sent to printer", | ||||
|         "preview": "Preview", | ||||
|         "your_preview_will_appear_here": "Your preview will appear here", | ||||
|         "quick_markdown_reference": "Quick Markdown Cheatsheet", | ||||
|         "bold": "Bold:", | ||||
|         "italic": "Italic:", | ||||
|         "bold_and_italic": "Bold and Italic:", | ||||
|         "large_header": "Large Header:", | ||||
|         "medium_header": "Medium Header:", | ||||
|         "small_header": "Small Header:", | ||||
|         "bullet_list": "Bullet List:", | ||||
|         "numbered_list": "Numbered List:", | ||||
|         "image": "Image:", | ||||
|         "line_break": "Line break:", | ||||
|         "uploaded_image": "Uploaded image:", | ||||
|         "generate_btn": "🖼️ Generate", | ||||
|         "print_btn": "🖨️ Print", | ||||
|         "text": "text", | ||||
|         "title": "Title", | ||||
|         "element": "Element", | ||||
|         "alt_text": "Alt text", | ||||
|         "image_url": "Image URL", | ||||
|         "leave_an_empty_line": "Leave an empty line", | ||||
|         "uses_the_uploaded_image": "(uses the uploaded image)", | ||||
|         "no_dithering": "No dithering" | ||||
|     }, | ||||
|     "es": { | ||||
|         "catnote": "CatNote", | ||||
|         "enter_markdown_here": "Ingrese Markdown aquí...", | ||||
|         "upload_image_optional": "Subir imagen (opcional):", | ||||
|         "banner_mode_vertical": "Modo Banner (vertical)", | ||||
|         "dithering": "Dithering:", | ||||
|         "intensity": "Intensidad:", | ||||
|         "rotate_image": "Rotar imagen:", | ||||
|         "print_mode": "Modo de impresión:", | ||||
|         "printing": "Imprimiendo...", | ||||
|         "sent_to_printer": "Enviado a impresora", | ||||
|         "preview": "Vista previa", | ||||
|         "your_preview_will_appear_here": "Su vista previa aparecerá aquí", | ||||
|         "quick_markdown_reference": "Referencia rápida de Markdown", | ||||
|         "bold": "Negrita:", | ||||
|         "italic": "Cursiva:", | ||||
|         "bold_and_italic": "Negrita y cursiva:", | ||||
|         "large_header": "Encabezado grande:", | ||||
|         "medium_header": "Encabezado mediano:", | ||||
|         "small_header": "Encabezado chico:", | ||||
|         "bullet_list": "Lista con viñetas:", | ||||
|         "numbered_list": "Lista numerada:", | ||||
|         "image": "Imágen:", | ||||
|         "line_break": "Salto de línea:", | ||||
|         "uploaded_image": "Imagen subida:", | ||||
|         "generate_btn": "🖼️ Generar", | ||||
|         "print_btn": "🖨️ Imprimir", | ||||
|         "text": "texto", | ||||
|         "title": "Título", | ||||
|         "element": "Elemento", | ||||
|         "alt_text": "Texto alternativo", | ||||
|         "image_url": "URL de la imagen", | ||||
|         "leave_an_empty_line": "Deje una línea vacía", | ||||
|         "uses_the_uploaded_image": "(usa la imagen cargada)", | ||||
|         "no_dithering": "Sin dithering" | ||||
|     } | ||||
| } | ||||
							
								
								
									
										645
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										645
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,645 @@ | ||||
| <!doctype html> | ||||
| <html> | ||||
| 
 | ||||
| <head> | ||||
|   <title>{{ t['catnote'] }}</title> | ||||
|   <link rel="manifest" href="/static/manifest.json"> | ||||
|   <style> | ||||
|     body { | ||||
|       font-family: sans-serif; | ||||
|       background: #181c1f; | ||||
|       color: #ddd; | ||||
|       min-height: 100vh; | ||||
|       margin: 0; | ||||
|     } | ||||
| 
 | ||||
|     .centered-flex { | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       align-items: flex-start; | ||||
|       height: 100vh; | ||||
|       gap: 2.6em; | ||||
|       padding-top: 2em; | ||||
|     } | ||||
| 
 | ||||
|     .form-card { | ||||
|       background: #22282c; | ||||
|       padding: 2em 2em 1em 2em; | ||||
|       border-radius: 1.5em; | ||||
|       box-shadow: 0 0 12px 0 #000a; | ||||
|       min-width: 410px; | ||||
|       max-width: 450px; | ||||
|       margin-right: 0; | ||||
|       box-sizing: border-box; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|     } | ||||
| 
 | ||||
|     .markdown-ref { | ||||
|       background: #22282c; | ||||
|       border-radius: 1.2em; | ||||
|       box-shadow: 0 0 12px 0 #000a; | ||||
|       padding: 2em 2em 1.5em 2em; | ||||
|       color: #b6c8e0; | ||||
|       font-size: 1.08rem; | ||||
|       font-family: inherit; | ||||
|       box-sizing: border-box; | ||||
|       width: 100%; | ||||
|       max-width: 410px; | ||||
|     } | ||||
| 
 | ||||
|     .markdown-ref h4 { | ||||
|       margin: 0 0 0.4em 0; | ||||
|       color: #88e0ff; | ||||
|       font-size: 1.12rem; | ||||
|       font-weight: 700; | ||||
|       letter-spacing: 0.01em; | ||||
|     } | ||||
| 
 | ||||
|     .markdown-ref ul { | ||||
|       margin: 0.6em 0 0.3em 0.1em; | ||||
|       padding-left: 1.1em; | ||||
|     } | ||||
| 
 | ||||
|     .markdown-ref li { | ||||
|       margin-bottom: 0.36em; | ||||
|     } | ||||
| 
 | ||||
|     .markdown-ref code { | ||||
|       background: #181c1f; | ||||
|       border-radius: 0.5em; | ||||
|       padding: 0.12em 0.46em; | ||||
|       color: #aef6cb; | ||||
|       font-size: 0.98rem; | ||||
|       margin-left: 0.18em; | ||||
|     } | ||||
| 
 | ||||
|     textarea { | ||||
|       width: 100%; | ||||
|       height: 14em; | ||||
|       padding: 0.75em; | ||||
|       border-radius: 0.7em; | ||||
|       border: 1.5px solid #444; | ||||
|       background: #181c1f; | ||||
|       color: #eee; | ||||
|       font-size: 1.06rem; | ||||
|       margin-bottom: 1em; | ||||
|       resize: vertical; | ||||
|       font-family: 'DejaVu Sans Mono', 'Fira Mono', 'monospace'; | ||||
|       box-sizing: border-box; | ||||
|       margin-right: 0; | ||||
|       min-width: 0; | ||||
|     } | ||||
| 
 | ||||
|     .buttons { | ||||
|       display: flex; | ||||
|       gap: 1em; | ||||
|       justify-content: center; | ||||
|     } | ||||
| 
 | ||||
|     button[type=submit] { | ||||
|       background: linear-gradient(90deg, #8ee3c1, #35a7ff); | ||||
|       color: #222; | ||||
|       font-weight: bold; | ||||
|       font-size: 1.12rem; | ||||
|       border: none; | ||||
|       border-radius: 0.7em; | ||||
|       padding: 0.5em 1.6em; | ||||
|       margin-top: 0.1em; | ||||
|       box-shadow: 0 2px 12px #1116; | ||||
|       cursor: pointer; | ||||
|       transition: filter 0.2s, box-shadow 0.2s; | ||||
|       filter: brightness(1); | ||||
|       min-width: 150px; | ||||
|     } | ||||
| 
 | ||||
|     button[type=submit]:hover { | ||||
|       filter: brightness(1.12); | ||||
|       box-shadow: 0 4px 18px #2229; | ||||
|     } | ||||
| 
 | ||||
|     button[type=submit][name=print] { | ||||
|       background: linear-gradient(90deg, #ffeb3b, #ff9100); | ||||
|       color: #181c1f; | ||||
|     } | ||||
| 
 | ||||
|     .preview-card { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       background: #23282d; | ||||
|       padding: 2em 1.4em 1em 1.4em; | ||||
|       border-radius: 1.5em; | ||||
|       box-shadow: 0 0 12px 0 #0007; | ||||
|       min-width: 400px; | ||||
|       text-align: center; | ||||
|       box-sizing: border-box; | ||||
|       margin-left: 0; | ||||
|       align-items: center; | ||||
|     } | ||||
| 
 | ||||
|     img { | ||||
|       margin-top: 1em; | ||||
|       max-width: 100%; | ||||
|       background: #fff; | ||||
|       box-shadow: 0 0 8px 1px #111a; | ||||
|     } | ||||
| 
 | ||||
|     .status-msg { | ||||
|       margin: 0.6em 0 0.1em 0; | ||||
|       font-size: 1.12rem; | ||||
|       border-radius: 0.7em; | ||||
|       background: #294b3a; | ||||
|       color: #9fffc6; | ||||
|       padding: 0.55em 1.2em; | ||||
|       font-weight: 600; | ||||
|       box-shadow: 0 0 6px #183a24a0; | ||||
|       display: inline-block; | ||||
|       border-left: 4px solid #4ffab0; | ||||
|     } | ||||
| 
 | ||||
|     .status-err { | ||||
|       background: #48202c; | ||||
|       color: #ffdbe4; | ||||
|       border-left: 4px solid #ff6384; | ||||
|       box-shadow: 0 0 8px #22000690; | ||||
|     } | ||||
| 
 | ||||
|     label { | ||||
|       font-size: 1em; | ||||
|       padding: 0 1em 0 1em; | ||||
|     } | ||||
| 
 | ||||
|     input[type=file] { | ||||
|       margin: 0.5em 1em 1.5em 1em; | ||||
|     } | ||||
| 
 | ||||
|     input[type=file]::file-selector-button { | ||||
|       background: linear-gradient(90deg, #8ee3c1, #35a7ff); | ||||
|       color: #222; | ||||
|       font-weight: bold; | ||||
|       border: none; | ||||
|       padding: 0.5em 1.2em; | ||||
|       border-radius: 0.7em; | ||||
|       box-shadow: 0 2px 12px #1116; | ||||
|       cursor: pointer; | ||||
|       transition: filter 0.2s, box-shadow 0.2s; | ||||
|       margin-right: 0.5em; | ||||
|     } | ||||
| 
 | ||||
|     input[type=file]::file-selector-button:hover { | ||||
|       filter: brightness(1.12); | ||||
|       box-shadow: 0 4px 18px #2229; | ||||
|     } | ||||
| 
 | ||||
|     .print-frame { | ||||
|       position: relative; | ||||
|       padding: 10px 0 10px 0; | ||||
|       margin-top: 1em; | ||||
|       overflow: hidden; | ||||
|       max-width: 424px; | ||||
|     } | ||||
| 
 | ||||
|     .print-frame::before, | ||||
|     .print-frame::after { | ||||
|       content: ""; | ||||
|       position: absolute; | ||||
|       left: 0; | ||||
|       width: 100%; | ||||
|       height: 10px; | ||||
|       background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDQuMjMzMyA0LjIzMzMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIHRyYW5zZm9ybT0ibWF0cml4KC4wMzYxMzUgMCAwIC4wNDE3MjUgMS4xNTY4IDEuMTI4OSkiIGQ9Im04NS4xNDEgNzQuNDAzLTExNy4xNS0xZS02IDU4LjU3Ny0xMDEuNDZ6IiBmaWxsPSIjZmZmIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBzdHJva2Utb3BhY2l0eT0iLjk3NjQ3IiBzdHJva2Utd2lkdGg9Ii4yNjQ1OCIvPgo8L3N2Zz4K') repeat-x left; | ||||
|       background-size: 10px 384px; | ||||
|       z-index: 1; | ||||
|     } | ||||
| 
 | ||||
|     .print-frame::before { | ||||
|       top: 1px; | ||||
|     } | ||||
| 
 | ||||
|     .print-frame::after { | ||||
|       bottom: 1px; | ||||
|       transform: rotate(180deg); | ||||
|     } | ||||
| 
 | ||||
|     .print-frame img { | ||||
|       background: #fff; | ||||
|       padding: 20px; | ||||
|       display: block; | ||||
|       margin: 0 auto; | ||||
|       box-shadow: none; | ||||
|       position: relative; | ||||
|       width: 100%; | ||||
|       max-width: 384px; | ||||
|       z-index: 2; | ||||
|     } | ||||
| 
 | ||||
|     textarea { | ||||
|       font-size: 1.16em !important; | ||||
|       padding: 1em; | ||||
|       min-height: 12em; | ||||
|     } | ||||
| 
 | ||||
|     .options { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       align-items: left; | ||||
|       justify-content: space-between; | ||||
|       margin-bottom: 1.2em; | ||||
|     } | ||||
| 
 | ||||
|     select { | ||||
|       background: #181c1f; | ||||
|       text-align: center; | ||||
|       text-align-last: center; | ||||
|       border: 1.5px solid #444; | ||||
|       color: #eee; | ||||
|       border-radius: 0.7em; | ||||
|       margin: 1em; | ||||
|       padding: 0.5em 1em; | ||||
|     } | ||||
| 
 | ||||
|     select option { | ||||
|       text-align: left; | ||||
|     } | ||||
| 
 | ||||
|     input[type=number] { | ||||
|       background: #181c1f; | ||||
|       text-align: center; | ||||
|       border: 1.5px solid #444; | ||||
|       color: #eee; | ||||
|       border-radius: 0.7em; | ||||
|       margin: 1em; | ||||
|       padding: 0.5em 1em; | ||||
|     } | ||||
| 
 | ||||
|     input[type=number]::-webkit-inner-spin-button, | ||||
|     input[type=number]::-webkit-outer-spin-button { | ||||
|       -webkit-appearance: none; | ||||
|       margin: 0; | ||||
|     } | ||||
| 
 | ||||
|     .options-dithering, | ||||
|     .options-intensity, | ||||
|     .options-rotation, | ||||
|     .options-printmode { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|     } | ||||
| 
 | ||||
|     #loading-box { | ||||
|       display: none; | ||||
|       flex-direction: column; | ||||
|       text-align: center; | ||||
|       box-sizing: border-box; | ||||
|       width: 100%; | ||||
|       padding: 1em; | ||||
|     } | ||||
| 
 | ||||
|     #printing-text { | ||||
|       color: #eee; | ||||
|       font-size: 1em; | ||||
|       font-weight: bold; | ||||
|       margin-bottom: 1em; | ||||
|     } | ||||
| 
 | ||||
|     .progress-bar { | ||||
|       position: relative; | ||||
|       width: 100%; | ||||
|       height: 18px; | ||||
|       background: #181c1f; | ||||
|       border: 1.5px solid #444; | ||||
|       border-radius: 12px; | ||||
|       overflow: hidden; | ||||
|       box-shadow: 0 2px 12px #1116; | ||||
|     } | ||||
| 
 | ||||
|     .progress-bar-inner { | ||||
|       width: 40%; | ||||
|       height: 100%; | ||||
|       background: linear-gradient(90deg, #8ee3c1, #35a7ff); | ||||
|       border-radius: 12px; | ||||
|       position: absolute; | ||||
|       left: -40%; | ||||
|       animation: indeterminate-bar 1.2s infinite | ||||
|     } | ||||
| 
 | ||||
|     @keyframes indeterminate-bar { | ||||
|       0% { | ||||
|         left: 0%; | ||||
|       } | ||||
| 
 | ||||
|       50% { | ||||
|         left: calc(100% - 40%); | ||||
|       } | ||||
| 
 | ||||
|       100% { | ||||
|         left: 0%; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     /* this next bit is a mess but it works */ | ||||
|     .options-bannermode { | ||||
|       display: flex; | ||||
|       align-items: left; | ||||
|       justify-content: space-between; | ||||
|       margin-left: 1em; | ||||
|       margin-right: 1em; | ||||
|     } | ||||
| 
 | ||||
|     .options-bannermode label { | ||||
|       font-size: 1em; | ||||
|       color: #eee; | ||||
|       margin-bottom: 1em; | ||||
|       display: block; | ||||
|     } | ||||
| 
 | ||||
|     .options-bannermode-inner { | ||||
|       display: block; | ||||
|       align-items: center; | ||||
|       position: relative; | ||||
|       padding-left: 35px; | ||||
|       margin-bottom: 12px; | ||||
|       cursor: pointer; | ||||
|       font-size: 1em; | ||||
|       -webkit-user-select: none; | ||||
|       -moz-user-select: none; | ||||
|       -ms-user-select: none; | ||||
|       user-select: none; | ||||
|     } | ||||
| 
 | ||||
|     /* Hide the browser's default checkbox */ | ||||
|     .options-bannermode-inner input { | ||||
|       position: absolute; | ||||
|       opacity: 0; | ||||
|       cursor: pointer; | ||||
|       height: 0; | ||||
|       width: 0; | ||||
|     } | ||||
| 
 | ||||
|     /* Create a custom checkbox */ | ||||
|     .checkmark { | ||||
|       position: absolute; | ||||
|       top: -25%; | ||||
|       left: 0; | ||||
|       height: 25px; | ||||
|       width: 25px; | ||||
|       background-color: #181c1f; | ||||
|       border-radius: 0.35em; | ||||
|       border: 1.5px solid #444; | ||||
|     } | ||||
| 
 | ||||
|     /* On mouse-over, add a grey background color */ | ||||
|     .options-bannermode-inner:hover input~.checkmark { | ||||
|       filter: brightness(1.12); | ||||
|     } | ||||
| 
 | ||||
|     /* When the checkbox is checked, add a blue background */ | ||||
|     .options-bannermode-inner input:checked~.checkmark { | ||||
|       background: linear-gradient(135deg, #8ee3c1, #35a7ff); | ||||
|     } | ||||
| 
 | ||||
|     /* Create the checkmark/indicator (hidden when not checked) */ | ||||
|     .checkmark:after { | ||||
|       content: ""; | ||||
|       position: absolute; | ||||
|       display: none; | ||||
|     } | ||||
| 
 | ||||
|     /* Show the checkmark when checked */ | ||||
|     .options-bannermode-inner input:checked~.checkmark:after { | ||||
|       display: block; | ||||
|     } | ||||
| 
 | ||||
|     /* Style the checkmark/indicator */ | ||||
|     .options-bannermode-inner .checkmark:after { | ||||
|       left: 9px; | ||||
|       top: 5px; | ||||
|       width: 5px; | ||||
|       height: 10px; | ||||
|       border: solid white; | ||||
|       border-width: 0 3px 3px 0; | ||||
|       -webkit-transform: rotate(45deg); | ||||
|       -ms-transform: rotate(45deg); | ||||
|       transform: rotate(45deg); | ||||
|     } | ||||
| 
 | ||||
|     #lang-switcher { | ||||
|       position: fixed; | ||||
|       right: 24px; | ||||
|       bottom: 18px; | ||||
|       z-index: 1000; | ||||
|       background: #23282d; | ||||
|       border-radius: 0.7em; | ||||
|       box-shadow: 0 2px 12px #1116; | ||||
|       padding: 0.5em 1em; | ||||
|       opacity: 0.95; | ||||
|     } | ||||
| 
 | ||||
|     #lang-select { | ||||
|       background: #181c1f; | ||||
|       color: #eee; | ||||
|       border: 1.5px solid #444; | ||||
|       border-radius: 0.7em; | ||||
|       padding: 0.4em 1em; | ||||
|       font-size: 1em; | ||||
|     } | ||||
| 
 | ||||
|     @media (max-width: 1000px) { | ||||
|       body { | ||||
|         font-size: 1.28em !important; | ||||
|       } | ||||
| 
 | ||||
|       h2, | ||||
|       h3, | ||||
|       h4 { | ||||
|         font-size: 2em !important; | ||||
|       } | ||||
| 
 | ||||
|       .centered-flex { | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         height: auto; | ||||
|         gap: 1.6em; | ||||
|       } | ||||
| 
 | ||||
|       .form-card, | ||||
|       .preview-card, | ||||
|       .markdown-ref { | ||||
|         font-size: 1.18em !important; | ||||
|         min-width: auto; | ||||
|         max-width: 90vw; | ||||
|         width: 100%; | ||||
|         margin: 0.8em 0; | ||||
|       } | ||||
| 
 | ||||
|       textarea { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: 1em; | ||||
|         min-height: 12em; | ||||
|       } | ||||
| 
 | ||||
|       .status-msg { | ||||
|         font-size: 1.25em !important; | ||||
|         padding: 0.8em 1.6em; | ||||
|       } | ||||
| 
 | ||||
|       button[type=submit] { | ||||
|         font-size: 1.3em !important; | ||||
|         padding: 0.9em 2.2em; | ||||
|       } | ||||
| 
 | ||||
|       input[type=file] { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: inherit 2.2em; | ||||
|       } | ||||
| 
 | ||||
|       label { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: inherit 2.2em; | ||||
|       } | ||||
| 
 | ||||
|       select { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: 0.6em 1.2em; | ||||
|       } | ||||
| 
 | ||||
|       input[type=number] { | ||||
|         font-size: 1.16em !important; | ||||
|         padding: 0.6em 1.2em; | ||||
|       } | ||||
| 
 | ||||
|       /* i'm so sorry, i'm not bothered to find out why or how this works */ | ||||
|       .options-bannermode-inner .checkmark { | ||||
|         top: -10%; | ||||
|       } | ||||
| 
 | ||||
|       .options-bannermode-inner .checkmark::after { | ||||
|         left: 13px; | ||||
|         top: 7px; | ||||
|         transform: rotate(45deg) scale(1.66); | ||||
|       } | ||||
| 
 | ||||
|       .checkmark { | ||||
|         width: 1.2em; | ||||
|         height: 1.2em; | ||||
|       } | ||||
| 
 | ||||
|       .options-bannermode-inner { | ||||
|         padding-left: 55px; | ||||
|       } | ||||
|     } | ||||
|   </style> | ||||
| </head> | ||||
| 
 | ||||
| <body> | ||||
|   <div class="centered-flex"> | ||||
|     <div class="form-card"> | ||||
|       <center> | ||||
|         <h2 style="margin-top:0.2em;">😺 {{ t['catnote'] }} 🖨️</h2> | ||||
|       </center> | ||||
|       <form method="post" enctype="multipart/form-data"> | ||||
|         <textarea name="md" placeholder="{{ t['enter_markdown_here'] }}">{{ default_md }}</textarea><br> | ||||
|         <label for="userimg" style="display: inline-block; margin-bottom: 0.5em;">{{ t['upload_image_optional'] | ||||
|           }}</label><br> | ||||
|         <input type="file" name="userimg" accept="image/png, image/jpeg"><br> | ||||
|         <div class="options"> | ||||
|           <div class="options-bannermode"> | ||||
|             <label class="options-bannermode-inner"> | ||||
|               <input type="checkbox" name="bannermode" value="1" {% if current_bannermode %}checked{% endif %}> | ||||
|               <span class="checkmark"></span> | ||||
|               {{ t['banner_mode_vertical'] }} | ||||
|             </label> | ||||
|           </div> | ||||
|           <div class="options-dithering"> | ||||
|             <label for="dithering">{{ t['dithering'] }}</label> | ||||
|             <select name="dithering"> | ||||
|               {% for label, mode in dithering_modes.items() %} | ||||
|               <option value="{{ mode }}" {% if mode==current_dithering %}selected="selected" {% endif %}>{{ label }} | ||||
|               </option> | ||||
|               {% endfor %} | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="options-intensity"> | ||||
|             <label for="intensity">{{ t['intensity'] }}</label> | ||||
|             <input type="number" name="intensity" value="85" min="0" max="100" step="1"> | ||||
|           </div> | ||||
|           <div class="options-rotation"> | ||||
|             <label for="rotation">{{ t['rotate_image'] }}</label> | ||||
|             <select name="rotation"> | ||||
|               <option value="0" {% if 0==current_rotation %}selected="selected" {% endif %}>0°</option> | ||||
|               <option value="90" {% if 90==current_rotation %}selected="selected" {% endif %}>90°</option> | ||||
|               <option value="180" {% if 180==current_rotation %}selected="selected" {% endif %}>180°</option> | ||||
|               <option value="270" {% if 270==current_rotation %}selected="selected" {% endif %}>270°</option> | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="options-printmode"> | ||||
|             <label for="printmode">{{ t['print_mode'] }}</label> | ||||
|             <select name="printmode"> | ||||
|               <option value="1bpp" {% if "1bpp"==current_printmode %}selected="selected" {% endif %}>1-bit</option> | ||||
|               <option value="4bpp" {% if "4bpp"==current_printmode %}selected="selected" {% endif %}>4-bit</option> | ||||
|             </select> | ||||
|           </div> | ||||
|           <div id="loading-box"> | ||||
|             <div id="printing-text">{{ t['printing'] }}</div> | ||||
|             <div class="progress-bar"> | ||||
|               <div class="progress-bar-inner"></div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="buttons"> | ||||
|           <button type="submit" name="generate">{{ t['generate_btn'] }}</button> | ||||
|           <button type="submit" name="print">{{ t['print_btn'] }}</button> | ||||
|         </div> | ||||
|       </form> | ||||
|       {% if printed %} | ||||
|       <div class="status-msg">✅ {{ t['sent_to_printer'] }}</div> | ||||
|       {% endif %} | ||||
|       {% if error %} | ||||
|       <div class="status-msg status-err">⚠️ {{ error }}</div> | ||||
|       {% endif %} | ||||
|     </div> | ||||
|     <div class="preview-card"> | ||||
|       {% if img %} | ||||
|       <h2 style="margin-top:0.1em; margin-bottom:0;">{{ t['preview'] }}</h2> | ||||
|       <div class="print-frame"> | ||||
|         <img src="data:image/png;base64,{{ img }}"> | ||||
|       </div> | ||||
|       {% else %} | ||||
|       <p style="opacity:.6;">{{ t['your_preview_will_appear_here'] }}</p> | ||||
|       {% endif %} | ||||
|     </div> | ||||
|     <div class="markdown-ref"> | ||||
|       <h4>{{ t['quick_markdown_reference'] }}</h4> | ||||
|       <ul> | ||||
|         <li><b>{{ t['bold'] }}</b> <code>**{{ t['text'] }}**</code></li> | ||||
|         <li><b>{{ t['italic'] }}</b> <code>*{{ t['text'] }}*</code></li> | ||||
|         <li><b>{{ t['bold_and_italic'] }}</b> <code>***{{ t['text'] }}***</code></li> | ||||
|         <li><b>{{ t['large_header'] }}</b> <code># {{ t['title'] }}</code></li> | ||||
|         <li><b>{{ t['medium_header'] }}</b> <code>## {{ t['title'] }}</code></li> | ||||
|         <li><b>{{ t['small_header'] }}</b> <code>### {{ t['title'] }}</code></li> | ||||
|         <li><b>{{ t['bullet_list'] }}</b> <code>- {{ t['element'] }}</code></li> | ||||
|         <li><b>{{ t['numbered_list'] }}</b> <code>1. {{ t['element'] }}</code></li> | ||||
|         <li><b>{{ t['image'] }}</b> <code>![{{ t['alt_text'] }}]({{ t['image_url'] }})</code></li> | ||||
|         <li><b>{{ t['line_break'] }}</b> {{ t['Leave an empty line'] }}</li> | ||||
|         <li><b>{{ t['uploaded_image'] }}</b> <code>!(img)</code> {{ t['uses_the_uploaded_image'] }}</li> | ||||
|       </ul> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div id="lang-switcher"> | ||||
|     <select id="lang-select"> | ||||
|       <option value="es" {% if lang=='es' %}selected{% endif %}>Español</option> | ||||
|       <option value="en" {% if lang=='en' %}selected{% endif %}>English</option> | ||||
|     </select> | ||||
|   </div> | ||||
|   <script> | ||||
|     document.getElementById('lang-select').addEventListener('change', function () { | ||||
|       window.location.search = '?lang=' + this.value; | ||||
|     }); | ||||
|     document.querySelector('form').addEventListener('submit', function (e) { | ||||
|       if (document.activeElement && document.activeElement.name === "print") { | ||||
|         document.getElementById('loading-box').style.display = 'flex'; | ||||
|       } | ||||
|     }); | ||||
|   </script> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user