Files
rdgen/rdgenerator/templates/generator.html
2025-09-25 13:02:58 -05:00

646 lines
31 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RustDesk Custom Client Builder</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Roboto', sans-serif;
background-color: #000;
color: #e0e0e0;
margin: 0;
padding: 20px;
}
.platform {
display: grid;
grid-template-columns: 1fr;
grid-gap: 20px;
margin: 0 auto;
padding: 20px;
max-width: 1200px;
}
.container {
display: grid;
grid-template-columns: 1fr 1fr; /* Adjust as needed */
grid-gap: 20px;
margin: 0 auto; /* Center the container horizontally */
padding: 20px;
max-width: 1200px;
}
.column {
flex: 50%;
}
h1 {
color: #fff;
text-align: center;
grid-column: 1 / -1;
}
h2 {
color: #fff;
margin-top: 0;
}
.section {
background-color: #111;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
flex: 50%;
}
label {
display: block;
margin-bottom: 5px;
color: #bbb;
}
input[type="text"], input[type="password"], select, textarea {
width: 100%;
padding: 8px;
margin-bottom: 10px;
background-color: #222;
border: 1px solid #444;
border-radius: 4px;
color: #fff;
}
input[type="radio"], input[type="checkbox"] {
margin-right: 5px;
}
button {
background-color: #0077ff;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0066cc;
}
.platform-icons {
display: flex;
justify-content: space-around;
margin-bottom: 20px;
}
.platform-icon {
font-size: 32px;
color: #bbb;
cursor: pointer;
transition: color 0.3s ease;
}
.platform-icon:hover, .platform-icon.active {
color: #2e52f7;
}
.checkbox-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.checkbox-group label {
display: flex;
align-items: center;
}
.preview-image {
max-width: 100%;
max-height: 100px;
margin-top: 10px;
}
.save-load-section-container { /* New container for fixed positioning */
position: fixed;
top: 20px; /* Adjust as needed */
left: 20px; /* Adjust as needed */
background-color: #111; /* Match your section background */
padding: 0px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 100; /* Ensure it's above other content */
}
.save-load-section {
display: none; /* Initially hidden */
}
.error {
color: red;
}
.errorlist {
color: red;
display: flex; /* Enable flexbox for centering */
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically (if needed) */
list-style: none; /* Remove bullet points if it's a list */
padding: 0; /* Remove default padding */
margin: 10px auto; /* Center the list itself, add some top/bottom margin */
width: fit-content; /* Make the width fit the content */
}
.errorlist li {
margin: 5px; /* Add some spacing between list items */
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.help-text {
color: #ffd700;
font-style: italic;
animation: blink 2s infinite;
padding: 5px;
border-radius: 4px;
display: inline-block;
margin-left: 10px;
}
.password-requirement {
display: none; /* Hidden by default */
color: orange;
font-size: 0.9em;
margin-top: 5px;
}
.sponsor-button {
display: inline-flex;
align-items: center;
background: linear-gradient(135deg, #00457C 0%, #0079C1 100%);
color: white;
padding: 12px 28px;
border-radius: 50px;
text-decoration: none;
font-weight: 600;
font-size: 16px;
letter-spacing: 0.5px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 69, 124, 0.2);
border: 2px solid rgba(255, 255, 255, 0.1);
text-transform: uppercase;
}
.sponsor-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 69, 124, 0.3);
background: linear-gradient(135deg, #005AA7 0%, #0095EA 100%);
border-color: rgba(255, 255, 255, 0.2);
}
.sponsor-button i {
margin-right: 12px;
font-size: 20px;
background: white;
color: #00457C;
padding: 8px;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.sponsor-button:hover i {
transform: rotate(360deg);
color: #0095EA;
}
</style>
</head>
<body>
<h1><i class="fas fa-cogs"></i> RustDesk Custom Client Builder</h1>
<form id="myForm" action="/generator" method="post" enctype="multipart/form-data">
<div class="save-load-section-container">
<div class="section">
<h2 id="saveLoadTitle">Save/Load Configuration <i class="fas fa-chevron-down"></i></h2>
<div class="save-load-section">
<button type="button" onclick="saveFormData()">Save Configuration</button>
<input type="file" id="fileInput" style="display:none;" accept=".json">
<button type="button" onclick="loadFormData()">Load Configuration</button>
</div>
</div>
</div>
{% if form.iconfile.errors %}
<ul class="errorlist">
{% for error in form.iconfile.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<div class="platform">
<h2><i class="fas fa-desktop"></i> Select Platform</h2>
<div class="platform-icons">
<i class="fab fa-windows platform-icon active" data-platform="windows"></i>
<i class="fab fa-linux platform-icon" data-platform="linux"></i>
<i class="fab fa-android platform-icon" data-platform="android"></i>
<i class="fab fa-apple platform-icon" data-platform="macos"></i>
</div>
<select name="platform" id="id_platform">
<option value="windows" selected>Windows 64Bit</option>
<option value="windows-x86">Windows 32Bit</option>
<option value="linux">Linux</option>
<option value="android">Android</option>
<option value="macos">macOS</option>
</select>
<label for="{{ form.version.id_for_label }}">Rustdesk Version:</label>
{{ form.version }}
{% if form.version.help_text %}
<span class="help-text">{{ form.version.help_text }}</span>
{% endif %}
<div class="help-text">{{ form.version.help_text }}</div>
<label for="{{ form.delayFix.id_for_label }}">{{ form.delayFix }} Fix connection delay when using third-party API</label>
</div>
</div>
<div class="container">
<div class="section">
<h2><i class="fas fa-sliders-h"></i> General</h2>
<label for="{{ form.exename.id_for_label }}">Name of the configuration (no spaces or special characters, English characters only):</label>
{{ form.exename }}<span id="filenameError" class="error"></span><br><br>
<label for="{{ form.appname.id_for_label }}">Custom Application Name (leave blank to use default):</label>
{{ form.appname }}<br><br>
<label for="{{ form.direction.id_for_label }}">Connection Type:</label>
{{ form.direction }}<br><br>
<label for="{{ form.installation.id_for_label }}">Disable Installation:</label>
{{ form.installation }}<br><br>
<label for="{{ form.settings.id_for_label }}">Disable Settings:</label>
{{ form.settings }}<br><br>
</div>
<div class="section">
<h2><i class="fas fa-server"></i> Custom Server</h2>
<label for="{{ form.serverIP.id_for_label }}">Host:</label>
{{ form.serverIP }}<br><br>
<label for="{{ form.key.id_for_label }}">Key:</label>
{{ form.key }}<br><br>
<label for="{{ form.apiServer.id_for_label }}">API:</label>
{{ form.apiServer }}<br><br>
<label for="{{ form.urlLink.id_for_label }}">Custom URL for links (replaces https://rustdesk.com):</label>
{{ form.urlLink }}<br><br>
<label for="{{ form.downloadLink.id_for_label }}">Custom URL for downloading updates (replaces https://rustdesk.com/download):</label>
{{ form.downloadLink }}<br><br>
<label for="{{ form.compname.id_for_label }}">Company name for copyright (replaces Purslane Ltd):</label>
{{ form.compname }}<br><br>
</div>
</div>
<div class="container">
<div class="section">
<h2><i class="fas fa-shield-alt"></i> Security</h2>
<label for="{{ form.runasadmin.id_for_label }}">Always run as Administrator?</label>
{{ form.runasadmin }}<br><br>
<label for="{{ form.passApproveMode.id_for_label }}">Password Approve mode:</label>
{{ form.passApproveMode }}<br><br>
<div id="passwordRequirement" class="password-requirement">To use the hide connection window feature, please set a permanent password.</div>
<label for="{{ form.permanentPassword.id_for_label }}">Set Permanent Password:</label>
{{ form.permanentPassword }} *The password is used as default, but can be changed by the client<br><br>
<label for="{{ form.denyLan.id_for_label }}">{{ form.denyLan }} Deny LAN discovery</label><br>
<label for="{{ form.enableDirectIP.id_for_label }}">{{ form.enableDirectIP }} Enable direct IP access</label><br>
<label for="{{ form.autoClose.id_for_label }}">{{ form.autoClose }} Automatically close incoming sessions on user inactivity</label><br>
<label for="{{ form.hidecm.id_for_label }}">{{ form.hidecm }} Allow hiding the connection window from remote screen.</label><br>
</div>
<div class="section">
<h2><i class="fas fa-paint-brush"></i> Visual</h2>
{{ form.iconbase64.as_hidden }}
{{ form.logobase64.as_hidden }}
<label for="{{ form.iconfile.id_for_label }}">Custom App Icon (in .png format)</label>
{{ form.iconfile }}<br><br>
<!-- <input type="file" name="iconfile" id="iconfile" accept="image/png"> -->
<div id="icon-preview"></div><br><br>
<label for="{{ form.logofile.id_for_label }}">Custom App Logo (in .png format)</label>
{{ form.logofile }}<br><br>
<!-- <input type="file" name="logofile" id="logofile" accept="image/png"> -->
<div id="logo-preview"></div><br><br>
<label for="{{ form.theme.id_for_label }}">Theme:</label>
{{ form.theme }} {{ form.themeDorO }} *Default sets the theme but allows the client to change it, Override sets the theme permanently.<br><br>
</div>
</div>
<div class="container">
<div class="section">
<h2><i class="fas fa-lock"></i> Permissions</h2>
The following Permissions can be set as default (the user can change the settings) or override (the settings cannot be changed).<br>
{{ form.permissionsDorO }}
<label for="{{ form.permissionsType.id_for_label }}">Permission type:</label>
{{ form.permissionsType }}<br><br>
<div class="checkbox-group">
<label for="{{ form.enableKeyboard.id_for_label }}">{{ form.enableKeyboard }} Enable keyboard/mouse</label>
<label for="{{ form.enableClipboard.id_for_label }}">{{ form.enableClipboard }} Enable clipboard</label>
<label for="{{ form.enableFileTransfer.id_for_label }}">{{ form.enableFileTransfer }} Enable file transfer</label>
<label for="{{ form.enableAudio.id_for_label }}">{{ form.enableAudio }} Enable audio</label>
<label for="{{ form.enableTCP.id_for_label }}">{{ form.enableTCP }} Enable TCP tunneling</label>
<label for="{{ form.enableRemoteRestart.id_for_label }}">{{ form.enableRemoteRestart }} Enable remote restart</label>
<label for="{{ form.enableRecording.id_for_label }}">{{ form.enableRecording }} Enable recording session</label>
<label for="{{ form.enableBlockingInput.id_for_label }}">{{ form.enableBlockingInput }} Enable blocking user input</label>
<label for="{{ form.enableRemoteModi.id_for_label }}">{{ form.enableRemoteModi }} Enable remote configuration modification</label>
<label for="{{ form.enablePrinter.id_for_label }}">{{ form.enablePrinter }} Enable remote printer</label>
<label for="{{ form.enableCamera.id_for_label }}">{{ form.enableCamera }} Enable remote camera</label>
<label for="{{ form.enableTerminal.id_for_label }}">{{ form.enableTerminal }} Enable remote terminal</label>
</div><br>
<h2><i class="fas fa-code"></i> Code Changes</h2>
<label for="{{ form.cycleMonitor.id_for_label }}">{{ form.cycleMonitor }} Add a button to cycle through available monitors to the minimized toolbar.</label><br>
<label for="{{ form.xOffline.id_for_label }}">{{ form.xOffline }} Display an X for offline devices in the addressbook.</label><br>
<label for="{{ form.removeNewVersionNotif.id_for_label }}">{{ form.removeNewVersionNotif }} Remove notification for new versions.</label><br>
</div>
<div class="section">
<h2><i class="fas fa-cog"></i> Other</h2>
<label for="{{ form.removeWallpaper.id_for_label }}">{{ form.removeWallpaper }} Remove wallpaper during incoming sessions</label><br>
<a href="https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/">Click here for a list of Default/Override settings</a>
<label for="{{ form.defaultManual.id_for_label }}">Default settings</label><br>
{{ form.defaultManual }}<br><br>
<label for="{{ form.overrideManual.id_for_label }}">Override settings</label><br>
{{ form.overrideManual }}<br><br>
</div>
</div>
<div class="platform">
<div class="section">
<button type="submit"><i class="fas fa-rocket"></i> Generate Custom Client</button>
</div>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="https://github.com/bryangerlach/rdgen"
target="_blank"
class="sponsor-button">
<i class="fab fa-github"></i>
Source Code on Github
</a>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="https://github.com/sponsors/bryangerlach?o=esb"
target="_blank"
class="sponsor-button">
<i class="fab fa-github"></i>
Donate
</a>
</div>
</form>
<script>
document.querySelectorAll('.platform-icon').forEach(icon => {
icon.addEventListener('click', function() {
document.querySelectorAll('.platform-icon').forEach(i => i.classList.remove('active'));
this.classList.add('active');
document.getElementById('id_platform').value = this.dataset.platform;
});
});
document.getElementById("{{ form.iconfile.id_for_label }}").addEventListener('change', function(event) {
previewImage(event.target, 'icon-preview');
});
document.getElementById("{{ form.logofile.id_for_label }}").addEventListener('change', function(event) {
previewImage(event.target, 'logo-preview');
});
document.getElementById("{{ form.hidecm.id_for_label }}").addEventListener('change',function() {
if (this.checked) {
document.getElementById("passwordRequirement").style.display = 'block';
document.getElementById("{{ form.permanentPassword.id_for_label }}").focus();
document.getElementById("{{ form.passApproveMode.id_for_label }}").value = 'password';
} else {
document.getElementById("passwordRequirement").style.display = 'none';
document.getElementById("{{ form.passApproveMode.id_for_label }}").value = 'password-click';
}
});
const enableKeyboard = document.getElementById("{{ form.enableKeyboard.id_for_label }}");
const enableClipboard = document.getElementById("{{ form.enableClipboard.id_for_label }}");
const enableFileTransfer = document.getElementById("{{ form.enableFileTransfer.id_for_label }}");
const enableAudio = document.getElementById("{{ form.enableAudio.id_for_label }}");
const enableTCP = document.getElementById("{{ form.enableTCP.id_for_label }}");
const enableRemoteRestart = document.getElementById("{{ form.enableRemoteRestart.id_for_label }}");
const enableRecording = document.getElementById("{{ form.enableRecording.id_for_label }}");
const enableBlockingInput = document.getElementById("{{ form.enableBlockingInput.id_for_label }}");
const enableRemoteModi = document.getElementById("{{ form.enableRemoteModi.id_for_label }}");
const enablePrinter = document.getElementById("{{ form.enablePrinter.id_for_label }}");
const enableCamera = document.getElementById("{{ form.enableCamera.id_for_label }}");
const enableTerminal = document.getElementById("{{ form.enableTerminal.id_for_label }}");
document.getElementById("{{ form.permissionsType.id_for_label }}").addEventListener('change', function() {
if (this.value === 'full') {
enableKeyboard.checked = true;
enableClipboard.checked = true;
enableFileTransfer.checked = true;
enableAudio.checked = true;
enableTCP.checked = true;
enableRemoteRestart.checked = true;
enableRecording.checked = true;
enableBlockingInput.checked = true;
enableRemoteModi.checked = true;
enablePrinter.checked = true;
enableCamera.checked = true;
enableTerminal.checked = true;
enableKeyboard.disabled = true;
enableClipboard.disabled = true;
enableFileTransfer.disabled = true;
enableAudio.disabled = true;
enableTCP.disabled = true;
enableRemoteRestart.disabled = true;
enableRecording.disabled = true;
enableBlockingInput.disabled = true;
enableRemoteModi.disabled = true;
enablePrinter.disabled = true;
enableCamera.disabled = true;
enableTerminal.disable = true;
} else if (this.value === 'view') {
enableKeyboard.checked = false;
enableClipboard.checked = false;
enableFileTransfer.checked = false;
enableAudio.checked = false;
enableTCP.checked = false;
enableRemoteRestart.checked = false;
enableRecording.checked = false;
enableBlockingInput.checked = false;
enableRemoteModi.checked = false;
enablePrinter.checked = false;
enableCamera.checked = false;
enableTerminal.checked = false;
enableKeyboard.disabled = true;
enableClipboard.disabled = true;
enableFileTransfer.disabled = true;
enableAudio.disabled = true;
enableTCP.disabled = true;
enableRemoteRestart.disabled = true;
enableRecording.disabled = true;
enableBlockingInput.disabled = true;
enableRemoteModi.disabled = true;
enablePrinter.disabled = true;
enableCamera.disabled = true;
enableTerminal.disable = true;
} else if (this.value === 'custom') {
enableKeyboard.disabled = false;
enableClipboard.disabled = false;
enableFileTransfer.disabled = false;
enableAudio.disabled = false;
enableTCP.disabled = false;
enableRemoteRestart.disabled = false;
enableRecording.disabled = false;
enableBlockingInput.disabled = false;
enableRemoteModi.disabled = false;
enablePrinter.checked = false;
enableCamera.checked = false;
enableTerminal.checked = false;
}
});
function previewImage(input, previewContainerId) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
var img = document.createElement('img');
img.src = e.target.result;  
img.style.maxWidth = '300px';
img.style.maxHeight = '60px';
document.getElementById(previewContainerId).innerHTML = '';
document.getElementById(previewContainerId).appendChild(img);
};
reader.readAsDataURL(input.files[0]);
}
}
const saveLoadTitle = document.getElementById("saveLoadTitle");
const saveLoadSection = document.querySelector(".save-load-section");
saveLoadTitle.addEventListener("click", () => {
if (!saveLoadSection.style.display || saveLoadSection.style.display === "none") {
saveLoadSection.style.display = "block";
} else {
saveLoadSection.style.display = "none";
}
const icon = saveLoadTitle.querySelector("i");
icon.classList.toggle("fa-chevron-down");
icon.classList.toggle("fa-chevron-up");
});
async function saveFormData() {
const filename = document.getElementById("{{ form.exename.id_for_label }}").value;
if (!filename) {
document.getElementById("filenameError").textContent = "Filename is required.";
return;
} else {
document.getElementById("filenameError").textContent = "";
}
const formData = {};
const form = document.getElementById("myForm"); // Get the form element
// 1. Use FormData for robust data collection (handles most input types):
const formDataObj = new FormData(form); // Create a FormData object
formDataObj.forEach((value, key) => {
formData[key] = value; // Add each key/value pair to the formData object.
});
// 2. Handle select elements separately (more reliable):
const selectElements = form.querySelectorAll('select');
selectElements.forEach(select => {
formData[select.name] = select.value;
});
// 3. Handle checkboxes and radio buttons (important!):
const checkboxRadioElements = form.querySelectorAll('input[type="checkbox"], input[type="radio"]');
checkboxRadioElements.forEach(input => {
if (input.name) { //only add to form data if it has a name
if (input.checked) {
formData[input.name] = input.value;
} else if (!formData.hasOwnProperty(input.name) && input.type === 'checkbox') { //if it's a checkbox and it's not checked, add it as false
formData[input.name] = false;
}
}
});
// 4. Handle icon and logo
const iconPreview = document.getElementById('icon-preview');
if (iconPreview.firstChild && iconPreview.firstChild.src.startsWith('data:image/png;base64')) {
formData.iconfile = iconPreview.firstChild.src; // Use existing base64
} else { //if it's a file upload
const iconFile = document.getElementById("{{ form.iconfile.id_for_label }}").files[0];
if (iconFile) {
formData.iconfile = await readFileAsBase64(iconFile);
}
}
const logoPreview = document.getElementById('logo-preview');
if (logoPreview.firstChild && logoPreview.firstChild.src.startsWith('data:image/png;base64')) {
formData.logofile = logoPreview.firstChild.src; // Use existing base64
} else { //if it's a file upload
const logoFile = document.getElementById("{{ form.logofile.id_for_label }}").files[0];
if (logoFile) {
formData.logofile = await readFileAsBase64(logoFile);
}
}
const jsonData = JSON.stringify(formData, null, 2);
const blob = new Blob([jsonData], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename + ".json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
async function readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => resolve(event.target.result);
reader.onerror = (error) => reject(error);
reader.readAsDataURL(file);
});
}
function loadFormData() {
const fileInput = document.getElementById("fileInput");
fileInput.click();
fileInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const formData = JSON.parse(e.target.result);
for (const key in formData) {
// More robust selector: checks for name OR id
const elements = document.querySelectorAll(`[name="${key}"], [id="${key}"]`);
if (elements.length > 0) { // Check if any element(s) exist
elements.forEach(element => { // Loop through all matching elements (important for radios)
if (element.type === 'radio') {
if (element.value === String(formData[key])) { // Compare value, crucial for radios
element.checked = true;
} else {
element.checked = false; // Uncheck others in the group
}
} else if (element.type === 'checkbox') {
element.checked = formData[key];
} else if (element.type !== 'file') {
element.value = formData[key];
}
// Handle image previews (as before)
if (key === 'iconfile' && formData[key]) {
document.getElementById('id_iconbase64').value = formData[key];
document.getElementById('icon-preview').innerHTML = `<img src="${formData[key]}" style="max-width: 300px; max-height: 60px;">`;
}
if (key === 'logofile' && formData[key]) {
document.getElementById('id_logobase64').value = formData[key];
document.getElementById('logo-preview').innerHTML = `<img src="${formData[key]}" style="max-width: 300px; max-height: 60px;">`;
}
});
}
}
} catch (error) {
alert("Error loading data. Invalid JSON file.");
}
};
reader.readAsText(file);
}
});
}
</script>
</body>
</html>