Pathl.Stona/templates/editor.html
2026-01-30 21:41:10 +01:00

898 lines
32 KiB
HTML

<!doctype html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>QL Editor - {{ project }} - {{ selected_file }}</title>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.navbar {
box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
}
.editor-container {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 20px;
}
.file-list {
background: #f8f9fa;
border-right: 1px solid #dee2e6;
min-height: 500px;
}
.file-item {
padding: 10px 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: all 0.2s;
}
.file-item:hover {
background-color: #e9ecef;
}
.file-item.active {
background-color: #007bff;
color: white;
}
#codeEditor {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
border: none;
padding: 20px;
width: 100%;
min-height: 400px;
resize: vertical;
background: #f8f9fa;
}
#codeEditor:focus {
outline: none;
background: white;
box-shadow: inset 0 0 0 1px #007bff;
}
.console-output {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 5px;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
border: 1px solid #333;
}
.console-command {
color: #4ec9b0;
font-weight: bold;
}
.console-response {
color: #ce9178;
}
.console-error {
color: #f44747;
}
.console-success {
color: #6a9955;
}
.console-info {
color: #9cdcfe;
}
.status-badge {
font-size: 0.75rem;
padding: 2px 8px;
}
.editor-toolbar {
background: linear-gradient(to right, #f8f9fa, #e9ecef);
border-bottom: 1px solid #dee2e6;
padding: 10px 15px;
}
.btn-editor {
padding: 5px 12px;
font-size: 0.875rem;
border-radius: 4px;
transition: all 0.2s;
}
.btn-editor:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<!-- Nawigacja -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">
<i class="bi bi-code-square me-2"></i>QL Platform
</a>
<div class="navbar-text text-light">
<span class="badge bg-light text-dark me-2">
<i class="bi bi-person"></i> {{ username }}
</span>
<span class="badge bg-light text-dark me-2">
<i class="bi bi-folder"></i> {{ project }}
</span>
<span class="badge bg-light text-dark">
<i class="bi bi-file-earmark-code"></i> {{ selected_file }}
</span>
</div>
<div class="d-flex">
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-light btn-sm me-2">
<i class="bi bi-grid"></i> Dashboard
</a>
<a href="{{ url_for('logout') }}" class="btn btn-outline-light btn-sm">
<i class="bi bi-box-arrow-right"></i> Wyloguj
</a>
</div>
</div>
</nav>
<div class="container-fluid mt-3">
<div class="row">
<!-- Lewy panel - pliki -->
<div class="col-md-3">
<div class="card shadow-sm mb-3">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="bi bi-files"></i> Pliki projektu
</h6>
<span class="badge bg-secondary">{{ files|length }}</span>
</div>
<div class="list-group list-group-flush" id="fileList">
{% for file in files %}
<a href="/editor/{{ username }}/{{ project }}?file={{ file }}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center {% if file == selected_file %}active bg-primary{% endif %}"
data-filename="{{ file }}">
<div>
<i class="bi
{% if file.endswith('.ql') %}bi-file-earmark-code text-primary
{% elif file.endswith('.md') %}bi-file-earmark-text text-success
{% elif file.endswith('.css') %}bi-file-earmark-text text-info
{% elif file.endswith('.js') %}bi-file-earmark-code text-warning
{% elif file.endswith('.json') %}bi-file-earmark-text text-secondary
{% else %}bi-file-earmark text-muted{% endif %} me-2">
</i>
{{ file }}
</div>
<small class="{% if file == selected_file %}text-white{% else %}text-muted{% endif %}">
{{ file_size(file) }}
</small>
</a>
{% endfor %}
</div>
<div class="card-footer bg-light">
<div class="d-grid gap-2">
<button id="newFileBtn" class="btn btn-outline-primary btn-sm">
<i class="bi bi-plus-circle"></i> Nowy plik
</button>
</div>
</div>
</div>
<!-- Statystyki -->
<div class="card shadow-sm">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-graph-up"></i> Statystyki</h6>
<div class="small">
<div class="d-flex justify-content-between">
<span>Pliki:</span>
<span class="badge bg-secondary">{{ files|length }}</span>
</div>
<div class="d-flex justify-content-between mt-1">
<span>Rozmiar projektu:</span>
<span class="badge bg-info">-</span>
</div>
<div class="d-flex justify-content-between mt-1">
<span>Ostatnia zmiana:</span>
<span class="badge bg-success">teraz</span>
</div>
</div>
</div>
</div>
</div>
<!-- Główny obszar - edytor -->
<div class="col-md-9">
<div class="editor-container">
<!-- Toolbar edytora -->
<div class="editor-toolbar d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">
<i class="bi bi-file-code text-primary me-2"></i>
{{ selected_file }}
<span id="unsavedIndicator"
class="badge bg-warning text-dark ms-2 d-none">Nie zapisano</span>
</h5>
</div>
<div class="btn-group">
<button id="saveBtn" class="btn btn-success btn-editor">
<i class="bi bi-save"></i> Zapisz
</button>
<button id="runBtn" class="btn btn-primary btn-editor">
<i class="bi bi-play"></i> Uruchom
</button>
<button id="formatBtn" class="btn btn-secondary btn-editor">
<i class="bi bi-braces"></i> Formatuj
</button>
<button id="downloadBtn" class="btn btn-outline-dark btn-editor">
<i class="bi bi-download"></i>
</button>
</div>
</div>
<!-- Edytor kodu -->
<div>
<textarea id="codeEditor" class="form-control border-0" rows="20"
placeholder="Wpisz swój kod QL tutaj...">{{ content }}</textarea>
</div>
<!-- Status bar -->
<div class="border-top bg-light p-2 d-flex justify-content-between align-items-center">
<div>
<small class="text-muted">
<span id="lineCount">Linie: {{ content.split('\n')|length }}</span> |
<span id="charCount">Znaki: {{ content|length }}</span>
</small>
</div>
<div>
<small class="text-muted" id="statusText">Gotowy</small>
</div>
</div>
</div>
<!-- Konsola -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="bi bi-terminal me-2"></i>Konsola
</h6>
<div>
<button id="clearConsoleBtn" class="btn btn-sm btn-outline-light me-2">
<i class="bi bi-trash"></i> Wyczyść
</button>
<button id="helpConsoleBtn" class="btn btn-sm btn-outline-light">
<i class="bi bi-question-circle"></i> Pomoc
</button>
</div>
</div>
<div class="card-body p-0">
<div id="consoleOutput" class="console-output">
<div class="console-success">=== QL Console v1.0 ===</div>
<div class="console-info">Projekt: {{ project }}</div>
<div class="console-info">Wpisz 'help' aby uzyskać pomoc</div>
<div class="console-info">Wpisz 'ls' aby zobaczyć pliki</div>
</div>
<div class="input-group p-3 bg-secondary bg-opacity-10">
<span class="input-group-text bg-dark text-success border-dark">
<i class="bi bi-terminal"></i> >
</span>
<input type="text" id="consoleInput" class="form-control bg-dark text-light border-dark"
placeholder="Wpisz komendę (help, ls, run, clear...)" autocomplete="off">
<button id="consoleSend" class="btn btn-success">
<i class="bi bi-arrow-return-left"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal do tworzenia nowego pliku -->
<div class="modal fade" id="newFileModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-file-earmark-plus"></i> Nowy plik</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="fileName" class="form-label">Nazwa pliku:</label>
<input type="text" class="form-control" id="fileName"
placeholder="np. program.ql, utils.js, style.css">
<div class="form-text">
Użyj odpowiedniego rozszerzenia: .ql, .js, .css, .md, .txt
</div>
</div>
<div class="mb-3">
<label for="fileContent" class="form-label">Zawartość (opcjonalnie):</label>
<textarea class="form-control" id="fileContent" rows="5" placeholder="// Twój kod..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
<button type="button" class="btn btn-primary" id="createFileBtn">
<i class="bi bi-plus-circle"></i> Utwórz
</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
// =================== GLOBALNE ZMIENNE ===================
const username = '{{ username }}';
const project = '{{ project }}';
const currentFile = '{{ selected_file }}';
let isDirty = false;
let autoSaveTimer = null;
// =================== INICJALIZACJA ===================
document.addEventListener('DOMContentLoaded', function () {
console.log('QL Editor initialized for:', username, project, currentFile);
initEditor();
initConsole();
initEventListeners();
updateStats();
});
// =================== FUNKCJE EDYTORA ===================
function initEditor() {
const editor = document.getElementById('codeEditor');
// Śledź zmiany w edytorze
editor.addEventListener('input', function () {
isDirty = true;
document.getElementById('unsavedIndicator').classList.remove('d-none');
updateStats();
updateStatus('Masz niezapisane zmiany');
});
// Skróty klawiaturowe
editor.addEventListener('keydown', function (e) {
// Ctrl+S - zapisz
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveFile();
}
// Ctrl+Enter - uruchom
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
runCode();
}
// Ctrl+F - formatuj
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
formatCode();
}
});
// Auto-zapis co 30 sekund
autoSaveTimer = setInterval(function () {
if (isDirty) {
autoSave();
}
}, 30000);
console.log('Editor initialized');
}
function updateStats() {
const editor = document.getElementById('codeEditor');
const lines = editor.value.split('\n').length;
const chars = editor.value.length;
document.getElementById('lineCount').textContent = `Linie: ${lines}`;
document.getElementById('charCount').textContent = `Znaki: ${chars}`;
}
function updateStatus(message) {
document.getElementById('statusText').textContent = message;
setTimeout(() => {
if (isDirty) {
document.getElementById('statusText').textContent = 'Masz niezapisane zmiany';
} else {
document.getElementById('statusText').textContent = 'Gotowy';
}
}, 3000);
}
// =================== FUNKCJE ZAPISU ===================
async function saveFile() {
const content = document.getElementById('codeEditor').value;
const filename = currentFile;
if (!content.trim()) {
showNotification('Plik jest pusty!', 'warning');
return;
}
try {
updateStatus('Zapisywanie...');
const response = await fetch(`/editor/${username}/${project}/save`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: filename,
content: content
})
});
const data = await response.json();
if (response.ok) {
showNotification('✓ Plik zapisany pomyślnie!', 'success');
isDirty = false;
document.getElementById('unsavedIndicator').classList.add('d-none');
updateStatus('Plik zapisany');
addConsoleMessage('✓ Plik zapisany', 'success');
} else {
throw new Error(data.error || 'Błąd zapisu');
}
} catch (error) {
showNotification(`✗ Błąd: ${error.message}`, 'danger');
updateStatus(`Błąd: ${error.message}`);
addConsoleMessage(`✗ Błąd zapisu: ${error.message}`, 'error');
}
}
function autoSave() {
if (!isDirty) return;
const content = document.getElementById('codeEditor').value;
const filename = currentFile;
fetch(`/editor/${username}/${project}/save`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: filename,
content: content
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'ok') {
addConsoleMessage('✓ Auto-zapisano plik', 'success');
}
})
.catch(error => {
console.error('Auto-save error:', error);
});
}
async function runCode() {
const code = document.getElementById('codeEditor').value;
if (!code.trim()) {
showNotification('Brak kodu do uruchomienia!', 'warning');
return;
}
try {
// Użyj prawdziwego backendu zamiast symulacji
const response = await fetch(`/editor/${username}/${project}/execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({code: code})
});
const data = await response.json();
console.log('Backend response:', data); // DODAJ TE LINIĘ DO DEBUGOWANIA
if (data.status === 'success') {
// WYŚWIETL TYLKO OUTPUT Z PRINT() - bez dodatkowych komunikatów
if (data.output && data.output.length > 0) {
console.log('Output array:', data.output); // DODAJ TE LINIĘ
data.output.forEach(line => {
console.log('Processing line:', line); // DODAJ TE LINIĘ
if (line && line.trim()) {
addConsoleMessage(line, 'response');
}
});
} else {
addConsoleMessage('(brak output)', 'info');
}
} else {
// Tylko błędy
if (data.output && data.output.length > 0) {
addConsoleMessage(data.output[0], 'error');
}
}
} catch (error) {
addConsoleMessage(`Błąd: ${error.message}`, 'error');
}
}
function formatCode() {
const editor = document.getElementById('codeEditor');
let code = editor.value;
// Proste formatowanie kodu QL
code = code
.replace(/\t/g, ' ') // Zamień taby na 4 spacje
.replace(/\n\s*\n\s*\n/g, '\n\n') // Usuń nadmiarowe puste linie
.replace(/\s+$/gm, ''); // Usuń białe znaki na końcu linii
// Wcięcia dla bloków
let indent = 0;
const lines = code.split('\n');
const formattedLines = [];
for (let line of lines) {
line = line.trim();
// Zmniejsz wcięcie przed zamknięciem bloku
if (line.includes('}')) {
indent = Math.max(0, indent - 1);
}
// Dodaj wcięcie
const indentedLine = ' '.repeat(indent) + line;
formattedLines.push(indentedLine);
// Zwiększ wcięcie po otwarciu bloku
if (line.includes('{')) {
indent++;
}
}
editor.value = formattedLines.join('\n');
isDirty = true;
updateStats();
addConsoleMessage('✓ Kod sformatowany', 'success');
showNotification('Kod sformatowany!', 'info');
}
// =================== FUNKCJE PLIKÓW ===================
async function createFile() {
const filename = document.getElementById('fileName').value.trim();
const content = document.getElementById('fileContent').value;
if (!filename) {
showNotification('Podaj nazwę pliku!', 'warning');
return;
}
// Sprawdź rozszerzenie
if (!filename.includes('.')) {
showNotification('Dodaj rozszerzenie pliku (np. .ql, .js, .css)', 'warning');
return;
}
try {
const response = await fetch(`/editor/${username}/${project}/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: filename,
content: content || `# ${filename}\n# Utworzono: ${new Date().toLocaleString()}\n`
})
});
const data = await response.json();
if (response.ok) {
showNotification(`✓ Utworzono plik: ${filename}`, 'success');
addConsoleMessage(`✓ Utworzono plik: ${filename}`, 'success');
// Zamknij modal
const modal = bootstrap.Modal.getInstance(document.getElementById('newFileModal'));
modal.hide();
// Odśwież stronę z nowym plikiem
setTimeout(() => {
window.location.href = `/editor/${username}/${project}?file=${filename}`;
}, 500);
} else {
throw new Error(data.error || 'Błąd tworzenia pliku');
}
} catch (error) {
showNotification(`✗ Błąd: ${error.message}`, 'danger');
}
}
function downloadFile() {
const content = document.getElementById('codeEditor').value;
const blob = new Blob([content], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentFile;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
addConsoleMessage(`✓ Pobrano plik: ${currentFile}`, 'success');
showNotification(`Pobrano plik: ${currentFile}`, 'info');
}
// =================== KONSOLA ===================
function initConsole() {
const consoleInput = document.getElementById('consoleInput');
const consoleOutput = document.getElementById('consoleOutput');
// Komendy konsoli
const commands = {
'help': () => {
return `Dostępne komendy:
help - wyświetla tę pomoc
ls - lista plików w projekcie
clear - czyści konsolę
run - uruchamia aktualny plik
status - pokazuje status projektu
time - pokazuje aktualny czas
echo [text] - wyświetla tekst
save - zapisuje aktualny plik
format - formatuje kod`;
},
'ls': async () => {
try {
const response = await fetch(`/editor/${username}/${project}/console`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'ls'
})
});
const data = await response.json();
return data.output || 'Brak odpowiedzi';
} catch (error) {
return `Błąd: ${error.message}`;
}
},
'clear': () => {
consoleOutput.innerHTML = '';
addConsoleMessage('=== QL Console v1.0 ===', 'success');
return 'Konsola wyczyszczona';
},
'run': () => {
runCode();
return 'Uruchamianie kodu...';
},
'status': () => {
const editor = document.getElementById('codeEditor');
const lines = editor.value.split('\n').length;
const chars = editor.value.length;
return `Projekt: ${project}
Plik: ${currentFile}
Linie: ${lines}
Znaki: ${chars}
Zmiany: ${isDirty ? 'TAK' : 'NIE'}
Rozmiar: ${fileSizeString(chars)}`;
},
'time': () => new Date().toLocaleString(),
'echo': (args) => args.join(' '),
'save': () => {
saveFile();
return 'Zapisywanie pliku...';
},
'format': () => {
formatCode();
return 'Formatowanie kodu...';
}
};
function fileSizeString(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
async function executeCommand(input) {
const parts = input.trim().split(' ');
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
if (commands[cmd]) {
try {
const result = await commands[cmd](args);
if (typeof result === 'string') {
return result;
}
} catch (error) {
return `Błąd wykonania: ${error.message}`;
}
} else if (cmd) {
return `Nieznana komenda: ${cmd}\nWpisz 'help' aby zobaczyć dostępne komendy`;
}
return '';
}
function processCommand() {
const input = consoleInput.value.trim();
if (!input) return;
// Wyświetl komendę
addConsoleLine(`> ${input}`, 'command');
// Wykonaj komendę
executeCommand(input).then(output => {
if (output) {
addConsoleLine(output);
}
});
// Wyczyść input
consoleInput.value = '';
}
// Event listeners dla konsoli
consoleInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
processCommand();
}
});
document.getElementById('consoleSend').addEventListener('click', processCommand);
document.getElementById('clearConsoleBtn').addEventListener('click', () => {
consoleOutput.innerHTML = '';
addConsoleLine('=== QL Console v1.0 ===', 'success');
addConsoleLine('Konsola wyczyszczona', 'success');
});
document.getElementById('helpConsoleBtn').addEventListener('click', () => {
addConsoleLine(`> help`, 'command');
addConsoleLine(commands.help());
});
// Auto-focus na konsolę
setTimeout(() => {
consoleInput.focus();
}, 100);
console.log('Console initialized');
}
function addConsoleLine(text, type = 'response') {
const consoleOutput = document.getElementById('consoleOutput');
const line = document.createElement('div');
line.className = `console-${type}`;
line.textContent = text;
consoleOutput.appendChild(line);
consoleOutput.scrollTop = consoleOutput.scrollHeight;
}
function addConsoleMessage(message, type = 'info') {
addConsoleLine(message, type);
}
// =================== EVENT LISTENERS ===================
function initEventListeners() {
// Zapisz plik
document.getElementById('saveBtn').addEventListener('click', saveFile);
// Uruchom kod
document.getElementById('runBtn').addEventListener('click', runCode);
// Formatuj kod
document.getElementById('formatBtn').addEventListener('click', formatCode);
// Nowy plik
document.getElementById('newFileBtn').addEventListener('click', () => {
const modal = new bootstrap.Modal(document.getElementById('newFileModal'));
modal.show();
// Ustaw focus na input
setTimeout(() => {
document.getElementById('fileName').focus();
}, 500);
});
// Utwórz plik (w modal)
document.getElementById('createFileBtn').addEventListener('click', createFile);
// Pobierz plik
document.getElementById('downloadBtn').addEventListener('click', downloadFile);
// Enter w modal
document.getElementById('fileName').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
createFile();
}
});
console.log('Event listeners initialized');
}
// =================== POMOCNICZE ===================
function showNotification(message, type = 'info') {
// Utwórz powiadomienie Bootstrap
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
alert.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
min-width: 300px;
max-width: 400px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
`;
alert.innerHTML = `
<div class="d-flex align-items-center">
<i class="bi ${type === 'success' ? 'bi-check-circle' :
type === 'warning' ? 'bi-exclamation-triangle' :
type === 'danger' ? 'bi-x-circle' : 'bi-info-circle'}
me-2"></i>
<span>${message}</span>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
// Auto-ukrywanie
setTimeout(() => {
if (alert.parentNode) {
bootstrap.Alert.getOrCreateInstance(alert).close();
}
}, 3000);
}
// Ostrzeżenie przed opuszczeniem strony
window.addEventListener('beforeunload', function (e) {
if (isDirty) {
e.preventDefault();
e.returnValue = 'Masz niezapisane zmiany. Czy na pewno chcesz opuścić stronę?';
return e.returnValue;
}
});
// Czyszczenie interwału przy zamknięciu
window.addEventListener('unload', function () {
if (autoSaveTimer) {
clearInterval(autoSaveTimer);
}
});
// =================== INIT LOG ===================
console.log('QL Editor script loaded successfully');
</script>
</body>
</html>