initial import
This commit is contained in:
parent
16991fe2c0
commit
fe1f00978b
234
app.py
Normal file
234
app.py
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
from flask import Flask, request, jsonify, redirect, url_for, session, render_template
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from flask_session import Session
|
||||||
|
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SECRET_KEY'] = 'your_secret_key' # Ändere dies zu einem echten geheimen Schlüssel
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///vocab.db'
|
||||||
|
app.config['SESSION_TYPE'] = 'filesystem'
|
||||||
|
app.config['SESSION_PERMANENT'] = False
|
||||||
|
app.config['SESSION_USE_SIGNER'] = True
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = 'login'
|
||||||
|
|
||||||
|
# Models
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String(150), nullable=False)
|
||||||
|
vocabularies = db.relationship('Vocabulary', backref='owner', lazy=True)
|
||||||
|
|
||||||
|
class Vocabulary(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
english = db.Column(db.String(100), nullable=False)
|
||||||
|
german = db.Column(db.String(100), nullable=False)
|
||||||
|
level = db.Column(db.Integer, nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_vocab(user_id):
|
||||||
|
vocab_list = Vocabulary.query.filter_by(user_id=user_id).all()
|
||||||
|
if not vocab_list:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Weight levels: Level 1 should be most common, Level 5 the least
|
||||||
|
weighted_vocab_list = []
|
||||||
|
for vocab in vocab_list:
|
||||||
|
weight = max(1, 6 - vocab.level) # Weight 5 for level 1, 4 for level 2, ..., 1 for level 5
|
||||||
|
weighted_vocab_list.extend([vocab] * weight)
|
||||||
|
|
||||||
|
return random.choice(weighted_vocab_list)
|
||||||
|
|
||||||
|
@app.route('/get_random_vocab', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_random_vocab_route():
|
||||||
|
vocab = get_random_vocab(current_user.id)
|
||||||
|
if vocab:
|
||||||
|
return jsonify({
|
||||||
|
'english': vocab.english,
|
||||||
|
'german': vocab.german,
|
||||||
|
'level': vocab.level,
|
||||||
|
'id': vocab.id,
|
||||||
|
}), 200
|
||||||
|
return jsonify({'error': 'No vocabulary found'}), 404
|
||||||
|
|
||||||
|
@app.route('/update_vocab_level/<int:id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_vocab_level(id):
|
||||||
|
vocab = Vocabulary.query.get(id)
|
||||||
|
if vocab and vocab.user_id == current_user.id:
|
||||||
|
if vocab.level < 5:
|
||||||
|
vocab.level += 1
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 200
|
||||||
|
return jsonify({'message': 'Max level reached'}), 200
|
||||||
|
return jsonify({'error': 'Not found or unauthorized'}), 404
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('start.html')
|
||||||
|
|
||||||
|
@app.route('/train')
|
||||||
|
@login_required
|
||||||
|
def train():
|
||||||
|
return render_template('train.html')
|
||||||
|
|
||||||
|
@app.route('/settings')
|
||||||
|
@login_required
|
||||||
|
def settings():
|
||||||
|
return render_template('settings.html')
|
||||||
|
|
||||||
|
@app.route('/vocab')
|
||||||
|
@login_required
|
||||||
|
def vocab():
|
||||||
|
return render_template('vocab.html')
|
||||||
|
|
||||||
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
return render_template('register.html', error='Username and password are required'), 400
|
||||||
|
|
||||||
|
existing_user = User.query.filter_by(username=username).first()
|
||||||
|
if existing_user:
|
||||||
|
return render_template('register.html', error='Username already taken'), 409 # 409 Conflict
|
||||||
|
|
||||||
|
hashed_password = generate_password_hash(password, method='pbkdf2:sha256')
|
||||||
|
new_user = User(username=username, password=hashed_password)
|
||||||
|
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
return render_template('register.html')
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if user and check_password_hash(user.password, password):
|
||||||
|
login_user(user)
|
||||||
|
return jsonify({'success': True}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Invalid credentials'}), 401
|
||||||
|
|
||||||
|
# GET request: render login page
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@app.route('/logout', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/add_vocab', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def add_vocab():
|
||||||
|
english = request.form.get('english')
|
||||||
|
german = request.form.get('german')
|
||||||
|
level = int(request.form.get('level', 1))
|
||||||
|
|
||||||
|
if not english or not german or not level:
|
||||||
|
return jsonify({'error': 'All fields are required'}), 400
|
||||||
|
|
||||||
|
vocab = Vocabulary(english=english, german=german, level=level, user_id=current_user.id)
|
||||||
|
try:
|
||||||
|
db.session.add(vocab)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 201
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/update_vocab/<int:id>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_vocab(id):
|
||||||
|
vocab = Vocabulary.query.get(id)
|
||||||
|
if not vocab or vocab.user_id != current_user.id:
|
||||||
|
return jsonify({'error': 'Not found or unauthorized'}), 404
|
||||||
|
|
||||||
|
english = request.json.get('english')
|
||||||
|
german = request.json.get('german')
|
||||||
|
level = request.json.get('level')
|
||||||
|
|
||||||
|
if english:
|
||||||
|
vocab.english = english
|
||||||
|
if german:
|
||||||
|
vocab.german = german
|
||||||
|
if level:
|
||||||
|
vocab.level = level
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 200
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/delete_vocab/<int:id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_vocab(id):
|
||||||
|
vocab = Vocabulary.query.get(id)
|
||||||
|
if not vocab or vocab.user_id != current_user.id:
|
||||||
|
return jsonify({'error': 'Not found or unauthorized'}), 404
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.delete(vocab)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 200
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/get_vocab', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_vocab():
|
||||||
|
vocab_list = Vocabulary.query.filter_by(user_id=current_user.id).all()
|
||||||
|
result = [{'id': v.id, 'english': v.english, 'german': v.german, 'level': v.level} for v in vocab_list]
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
@app.route('/change_username', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def change_username():
|
||||||
|
new_username = request.form.get('new_username')
|
||||||
|
|
||||||
|
if not new_username:
|
||||||
|
return jsonify({'error': 'Benutzername ist erforderlich'}), 400
|
||||||
|
|
||||||
|
if User.query.filter_by(username=new_username).first():
|
||||||
|
return jsonify({'error': 'Benutzername bereits vergeben'}), 409
|
||||||
|
|
||||||
|
current_user.username = new_username
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 200
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
app.run(debug=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
3
defindatabank.py
Normal file
3
defindatabank.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from app import db
|
||||||
|
db.create_all()
|
||||||
|
|
13
forms.py
Normal file
13
forms.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from app import db
|
||||||
|
from flask_login import UserMixin
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String(150), nullable=False)
|
||||||
|
notes = db.relationship('Note', backref='author', lazy=True)
|
||||||
|
|
||||||
|
class Note(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
BIN
instance/vocab.db
Normal file
BIN
instance/vocab.db
Normal file
Binary file not shown.
18
models.py
Normal file
18
models.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_login import UserMixin
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String(150), nullable=False)
|
||||||
|
notes = db.relationship('Note', backref='author', lazy=True)
|
||||||
|
|
||||||
|
class Note(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
151
static/script.js
Normal file
151
static/script.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
const canvas = document.getElementById('drawingCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const textElement = document.getElementById('text');
|
||||||
|
const wordDisplay = document.getElementById('wordDisplay');
|
||||||
|
const textInput = document.getElementById('textInput');
|
||||||
|
const submitAnswer = document.getElementById('submitAnswer');
|
||||||
|
const toggleInput = document.getElementById('toggleInput');
|
||||||
|
const clearCanvas = document.getElementById('clearCanvas');
|
||||||
|
const previewDrawing = document.getElementById('previewDrawing');
|
||||||
|
const recognizedText = document.getElementById('recognizedText');
|
||||||
|
|
||||||
|
let currentWord = null;
|
||||||
|
let currentInputMode = 'text'; // 'text' or 'drawing'
|
||||||
|
let isDrawing = false;
|
||||||
|
|
||||||
|
// Initialize input mode
|
||||||
|
function initializeInputMode() {
|
||||||
|
textInput.style.display = 'block';
|
||||||
|
canvas.style.display = 'none';
|
||||||
|
clearCanvas.style.display = 'none';
|
||||||
|
previewDrawing.style.display = 'none';
|
||||||
|
toggleInput.textContent = 'Zu Zeichnen wechseln';
|
||||||
|
recognizedText.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a new word
|
||||||
|
function loadWord() {
|
||||||
|
fetch('/get_random_vocab')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
wordDisplay.textContent = 'Keine Vokabeln gefunden.';
|
||||||
|
} else {
|
||||||
|
currentWord = data;
|
||||||
|
const isEnglish = Math.random() < 0.5;
|
||||||
|
wordDisplay.textContent = isEnglish ? `English: ${currentWord.english}` : `Deutsch: ${currentWord.german}`;
|
||||||
|
textElement.textContent = '';
|
||||||
|
textInput.value = '';
|
||||||
|
recognizedText.textContent = '';
|
||||||
|
if (currentInputMode === 'drawing') {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Fehler beim Laden der Vokabel:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch between input modes
|
||||||
|
toggleInput.addEventListener('click', () => {
|
||||||
|
if (currentInputMode === 'text') {
|
||||||
|
textInput.style.display = 'none';
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
clearCanvas.style.display = 'inline-block';
|
||||||
|
previewDrawing.style.display = 'inline-block';
|
||||||
|
toggleInput.textContent = 'Zu Texteingabe wechseln';
|
||||||
|
currentInputMode = 'drawing';
|
||||||
|
} else {
|
||||||
|
textInput.style.display = 'block';
|
||||||
|
canvas.style.display = 'none';
|
||||||
|
clearCanvas.style.display = 'none';
|
||||||
|
previewDrawing.style.display = 'none';
|
||||||
|
toggleInput.textContent = 'Zu Zeichnen wechseln';
|
||||||
|
currentInputMode = 'text';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the canvas
|
||||||
|
clearCanvas.addEventListener('click', () => {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
recognizedText.textContent = ''; // Clear the recognized text as well
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drawing functionality
|
||||||
|
canvas.addEventListener('mousedown', () => {
|
||||||
|
isDrawing = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mouseup', () => {
|
||||||
|
isDrawing = false;
|
||||||
|
ctx.beginPath();
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mousemove', draw);
|
||||||
|
|
||||||
|
function draw(event) {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.strokeStyle = 'black';
|
||||||
|
|
||||||
|
ctx.lineTo(event.clientX - canvas.offsetLeft, event.clientY - canvas.offsetTop);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(event.clientX - canvas.offsetLeft, event.clientY - canvas.offsetTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview the drawing recognition
|
||||||
|
previewDrawing.addEventListener('click', () => {
|
||||||
|
const dataURL = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
fetch(dataURL)
|
||||||
|
.then(res => res.blob())
|
||||||
|
.then(blob => {
|
||||||
|
Tesseract.recognize(
|
||||||
|
blob,
|
||||||
|
'eng',
|
||||||
|
{ logger: info => console.log(info) }
|
||||||
|
).then(({ data: { text } }) => {
|
||||||
|
recognizedText.textContent = `Erkannter Text: ${text.trim()}`;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Erkennen des Textes:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit answer
|
||||||
|
submitAnswer.addEventListener('click', () => {
|
||||||
|
let answer = '';
|
||||||
|
|
||||||
|
if (currentInputMode === 'text') {
|
||||||
|
answer = textInput.value.trim();
|
||||||
|
} else {
|
||||||
|
answer = recognizedText.textContent.replace('Erkannter Text:', '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAnswer(answer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the answer is correct
|
||||||
|
function checkAnswer(answer) {
|
||||||
|
const correctAnswer = (wordDisplay.textContent.startsWith('English:') ? currentWord.german : currentWord.english).trim().toLowerCase();
|
||||||
|
if (answer.toLowerCase() === correctAnswer) {
|
||||||
|
textElement.textContent = 'Richtig!';
|
||||||
|
textElement.style.color = 'green';
|
||||||
|
// Update the vocabulary level
|
||||||
|
fetch(`/update_vocab_level/${currentWord.id}`, { method: 'POST' })
|
||||||
|
.then(() => {
|
||||||
|
setTimeout(loadWord, 2000); // Show the result for 2 seconds before loading the next word
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
textElement.textContent = `Falsch! Die richtige Antwort ist: ${correctAnswer}`;
|
||||||
|
textElement.style.color = 'red';
|
||||||
|
setTimeout(loadWord, 3000); // Show the result for 3 seconds before loading the next word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the input mode and load the first word
|
||||||
|
initializeInputMode();
|
||||||
|
loadWord();
|
||||||
|
|
33
static/styles.css
Normal file
33
static/styles.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 1px solid #000;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
160
static/vocab.js
Normal file
160
static/vocab.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadVocab();
|
||||||
|
|
||||||
|
document.getElementById('addVocab').addEventListener('click', () => {
|
||||||
|
const englishInput = document.getElementById('englishInput').value;
|
||||||
|
const germanInput = document.getElementById('germanInput').value;
|
||||||
|
|
||||||
|
if (englishInput && germanInput) {
|
||||||
|
fetch('/add_vocab', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
english: englishInput,
|
||||||
|
german: germanInput,
|
||||||
|
level: 1 // Automatisch Level 1 setzen
|
||||||
|
})
|
||||||
|
}).then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
loadVocab();
|
||||||
|
|
||||||
|
document.getElementById('englishInput').value = '';
|
||||||
|
document.getElementById('germanInput').value = '';
|
||||||
|
} else {
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Fehler beim Hinzufügen der Vokabel:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Bitte alle Felder ausfüllen.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('backToHome').addEventListener('click', () => {
|
||||||
|
window.location.href = '/'; // Pfad zur Startseite anpassen
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadVocab() {
|
||||||
|
fetch('/get_vocab')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(vocabList => {
|
||||||
|
const tables = [1, 2, 3, 4, 5].map(level => document.getElementById(`vocab-list-${level}`));
|
||||||
|
tables.forEach(table => table.querySelector('tbody').innerHTML = '');
|
||||||
|
|
||||||
|
vocabList.forEach(vocab => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.dataset.id = vocab.id;
|
||||||
|
row.dataset.level = vocab.level; // Level in dataset speichern
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="editable">${vocab.english}</td>
|
||||||
|
<td class="editable">${vocab.german}</td>
|
||||||
|
<td>
|
||||||
|
<button class="edit-btn">Bearbeiten</button>
|
||||||
|
<button class="delete-btn" data-id="${vocab.id}">Löschen</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const targetTable = document.getElementById(`vocab-list-${vocab.level}`);
|
||||||
|
targetTable.querySelector('tbody').appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event-Listener für den „Löschen“-Button
|
||||||
|
document.querySelectorAll('.delete-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (event) => {
|
||||||
|
const vocabId = event.target.getAttribute('data-id');
|
||||||
|
fetch(`/delete_vocab/${vocabId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
}).then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
loadVocab();
|
||||||
|
} else {
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Fehler beim Löschen der Vokabel:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event-Listener für den „Bearbeiten“-Button
|
||||||
|
document.querySelectorAll('.edit-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (event) => {
|
||||||
|
const row = event.target.closest('tr');
|
||||||
|
const id = row.dataset.id;
|
||||||
|
const english = row.children[0].textContent;
|
||||||
|
const german = row.children[1].textContent;
|
||||||
|
const level = row.dataset.level || 1; // Level aus dem dataset lesen, Fallback auf 1
|
||||||
|
|
||||||
|
// Setze die Zeile in den Bearbeitungsmodus
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><input type="text" class="editable-input" value="${english}"></td>
|
||||||
|
<td><input type="text" class="editable-input" value="${german}"></td>
|
||||||
|
<td class="edit-controls">
|
||||||
|
<div class="level-input-container">
|
||||||
|
<label for="levelInput">Level:</label>
|
||||||
|
<input type="number" id="levelInput" class="level-input" min="1" max="5" value="${level}">
|
||||||
|
</div>
|
||||||
|
<button class="save-btn">Speichern</button>
|
||||||
|
<button class="cancel-btn">Abbrechen</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
row.querySelector('.save-btn').addEventListener('click', () => {
|
||||||
|
const englishInput = row.querySelector('.editable-input').value;
|
||||||
|
const germanInput = row.querySelectorAll('.editable-input')[1].value;
|
||||||
|
const levelInput = row.querySelector('#levelInput').value;
|
||||||
|
|
||||||
|
fetch(`/update_vocab/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
english: englishInput,
|
||||||
|
german: germanInput,
|
||||||
|
level: parseInt(levelInput, 10)
|
||||||
|
})
|
||||||
|
}).then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
loadVocab();
|
||||||
|
} else {
|
||||||
|
alert(data.error);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Fehler beim Aktualisieren der Vokabel:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
row.querySelector('.cancel-btn').addEventListener('click', () => {
|
||||||
|
loadVocab();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Fehler beim Laden der Vokabeln:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//sqlite3 anstatt cokkies
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
98
templates/login.html
Normal file
98
templates/login.html
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Anmelden</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 15px 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
background-color: #007bff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
.flash-message {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.flash-message.success {
|
||||||
|
color: green;
|
||||||
|
border: 1px solid green;
|
||||||
|
background-color: #d4edda;
|
||||||
|
}
|
||||||
|
.flash-message.error {
|
||||||
|
color: red;
|
||||||
|
border: 1px solid red;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Anmelden</h1>
|
||||||
|
<div class="container">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash-message {{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<form method="post" action="/login">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Benutzername:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Passwort:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button">Anmelden</button>
|
||||||
|
</form>
|
||||||
|
<a href="/register" class="button">Noch kein Konto? Registrieren</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
77
templates/register.html
Normal file
77
templates/register.html
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Register</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin-top: 30px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.form-group button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
background-color: #007bff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.form-group button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Register</h1>
|
||||||
|
<div class="container">
|
||||||
|
<form method="POST" action="/register">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" name="username" placeholder="Username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" name="password" placeholder="Password" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<p class="error">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
127
templates/settings.html
Normal file
127
templates/settings.html
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Einstellungen</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin: 20px auto;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 15px 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
background-color: #007bff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.form-group button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
background-color: #28a745;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.form-group button:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Einstellungen</h1>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Formular zum Ändern des Benutzernamens -->
|
||||||
|
<form id="username-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-username">Neuer Benutzername:</label>
|
||||||
|
<input type="text" id="new-username" name="new-username" placeholder="Neuer Benutzername" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Benutzernamen ändern</button>
|
||||||
|
<div id="username-error" class="error"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Formular zum Abmelden -->
|
||||||
|
<form id="logout-form" action="/logout" method="POST" style="display: inline;">
|
||||||
|
<button type="submit" class="button">Abmelden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('username-form').addEventListener('submit', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const newUsername = document.getElementById('new-username').value;
|
||||||
|
|
||||||
|
fetch('/change_username', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
new_username: newUsername
|
||||||
|
})
|
||||||
|
}).then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
document.getElementById('username-error').textContent = data.error;
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error changing username:', error);
|
||||||
|
document.getElementById('username-error').textContent = 'Ein Fehler ist aufgetreten.';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
47
templates/start.html
Normal file
47
templates/start.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vokabeltrainer</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 15px 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
background-color: #007bff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Vokabeltrainer</h1>
|
||||||
|
<div class="container">
|
||||||
|
<a href="/train" class="button">Vokabeln trainieren</a>
|
||||||
|
<a href="/settings" class="button">Einstellungen</a>
|
||||||
|
<a href="/vocab" class="button">Meine Vokabeln</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
89
templates/train.html
Normal file
89
templates/train.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vokabeln trainieren</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
#wordDisplay {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
#inputArea {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
#textInput {
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
border: 1px solid black;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 15px 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
background-color: #007bff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
#result {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: green;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
#recognizedText {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Vokabeln trainieren</h1>
|
||||||
|
<div id="wordDisplay">Lade...</div>
|
||||||
|
|
||||||
|
<div id="inputArea">
|
||||||
|
<input type="text" id="textInput" placeholder="Antwort eingeben">
|
||||||
|
<canvas id="drawingCanvas" width="600" height="400"></canvas>
|
||||||
|
<button id="clearCanvas" class="button">Zeichnung löschen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="toggleInput" class="button">Zu Zeichnen wechseln</button>
|
||||||
|
<button id="previewDrawing" class="button">Zeichnung prüfen</button>
|
||||||
|
<button id="submitAnswer" class="button">Abgeben</button>
|
||||||
|
|
||||||
|
<div id="recognizedText"></div>
|
||||||
|
|
||||||
|
<div id="result">
|
||||||
|
<pre id="text"></pre>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="button">Zurück zur Startseite</a>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@2.1.1/dist/tesseract.min.js"></script>
|
||||||
|
<script src="/static/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
268
templates/vocab.html
Normal file
268
templates/vocab.html
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Meine Vokabeln</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1200px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: white; /* Weiß für Tabelleninhalt */
|
||||||
|
}
|
||||||
|
.table-header {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.red .table-header, .red th { background-color: #d9534f; color: white; }
|
||||||
|
.orange .table-header, .orange th { background-color: #f0ad4e; color: white; }
|
||||||
|
.yellow .table-header, .yellow th { background-color: #ffd500; color: white; }
|
||||||
|
.green .table-header, .green th { background-color: #4dd64d; color: white; }
|
||||||
|
.darkgreen .table-header, .darkgreen th { background-color: #2f742f; color: white; }
|
||||||
|
.actions button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-right: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
/* Allgemeine Button-Stile */
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
/* Spezielle Button-Stile */
|
||||||
|
.edit-btn, .delete-btn, .save-btn, .cancel-btn, #moveUp, #moveDown, #moveFinish {
|
||||||
|
border-radius: 5px; /* Gleiche abgerundete Ecken für alle Buttons */
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.edit-btn {
|
||||||
|
background-color: #007bff; /* Blau */
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
background-color: #dc3545; /* Rot */
|
||||||
|
}
|
||||||
|
.save-btn {
|
||||||
|
background-color: #28a745; /* Grün */
|
||||||
|
}
|
||||||
|
.cancel-btn {
|
||||||
|
background-color: #dc3545; /* Rot */
|
||||||
|
}
|
||||||
|
/* Level-Input-Feld */
|
||||||
|
.level-input-container {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.level-input {
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
/* Bearbeitungsmodus-Stil */
|
||||||
|
.editing {
|
||||||
|
background-color: #e7f0ff; /* Hellblau als Hinweis für Bearbeitungsmodus */
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
#backToHome {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #007bff; /* Blau */
|
||||||
|
color: white;
|
||||||
|
margin: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stile für den Container */
|
||||||
|
#vocab-add-container {
|
||||||
|
margin-bottom: 20px; /* Abstand nach unten für die Tabelle */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stile für das Formular */
|
||||||
|
#add-vocab-form {
|
||||||
|
margin-bottom: 10px; /* Abstand nach unten für den Button */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stile für den "Hinzufügen"-Button */
|
||||||
|
#addVocab {
|
||||||
|
background-color: #007bff; /* Hintergrundfarbe des Buttons */
|
||||||
|
color: #fff; /* Schriftfarbe */
|
||||||
|
border: none; /* Kein Rand */
|
||||||
|
border-radius: 5px; /* Abgerundete Ecken */
|
||||||
|
padding: 10px 20px; /* Innenabstand (Padding) */
|
||||||
|
font-size: 16px; /* Schriftgröße */
|
||||||
|
cursor: pointer; /* Zeiger-Cursor beim Hover */
|
||||||
|
transition: background-color 0.3s ease; /* Übergangseffekt für Hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover-Effekt für den Button */
|
||||||
|
#addVocab:hover {
|
||||||
|
background-color: #0056b3; /* Dunklere Hintergrundfarbe beim Hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stile für die Tabelle */
|
||||||
|
#vocab-list {
|
||||||
|
width: 100%; /* Tabellenbreite auf 100% setzen */
|
||||||
|
border-collapse: collapse; /* Ränder zusammenführen */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stile für die Tabellenüberschriften */
|
||||||
|
#vocab-list th {
|
||||||
|
background-color: #f2f2f2; /* Hintergrundfarbe der Überschriften */
|
||||||
|
padding: 10px; /* Innenabstand der Überschriften */
|
||||||
|
text-align: left; /* Textausrichtung */
|
||||||
|
border-bottom: 2px solid #ddd; /* Untere Grenze der Überschriften */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stile für die Tabellenzellen */
|
||||||
|
#vocab-list td {
|
||||||
|
padding: 10px; /* Innenabstand der Zellen */
|
||||||
|
border-bottom: 1px solid #ddd; /* Untere Grenze der Zellen */
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button id="backToHome">Zurück zur Startseite</button>
|
||||||
|
<h1>Meine Vokabeln</h1>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Input-Bereich für Vokabeln -->
|
||||||
|
<div id="vocab-input">
|
||||||
|
<input type="text" id="englishInput" placeholder="Englisch">
|
||||||
|
<input type="text" id="germanInput" placeholder="Deutsch">
|
||||||
|
<!-- Das Level-Feld wird entfernt -->
|
||||||
|
<button id="addVocab">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabellen für Vokabeln -->
|
||||||
|
<table id="vocab-list-1" class="red">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<td colspan="4">Neue Vokabeln</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Englisch</th>
|
||||||
|
<th>Deutsch</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table id="vocab-list-2" class="orange">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<td colspan="4">Vokabeln, die noch nicht sitzen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Englisch</th>
|
||||||
|
<th>Deutsch</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table id="vocab-list-3" class="yellow">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<td colspan="4">Vokabeln, die fast sitzen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Englisch</th>
|
||||||
|
<th>Deutsch</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table id="vocab-list-4" class="green">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<td colspan="4">Vokabeln, die gut sitzen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Englisch</th>
|
||||||
|
<th>Deutsch</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table id="vocab-list-5" class="darkgreen">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<td colspan="4">Perfekte Vokabeln</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Englisch</th>
|
||||||
|
<th>Deutsch</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="static/vocab.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
168
test.txt
Normal file
168
test.txt
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
from flask import Flask, request, jsonify, redirect, url_for, session, render_template
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from flask_session import Session
|
||||||
|
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user, login_manager
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///vocab.db'
|
||||||
|
app.config['SECRET_KEY'] = 'your_secret_key' # Ändere dies zu einem echten geheimen Schlüssel
|
||||||
|
app.config['SESSION_TYPE'] = 'filesystem'
|
||||||
|
app.config['SESSION_PERMANENT'] = False
|
||||||
|
app.config['SESSION_USE_SIGNER'] = True
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
# Flask-Login Setup
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = 'login'
|
||||||
|
|
||||||
|
# Models
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String(150), nullable=False)
|
||||||
|
vocabularies = db.relationship('Vocabulary', backref='owner', lazy=True)
|
||||||
|
|
||||||
|
class Vocabulary(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
english = db.Column(db.String(100), nullable=False)
|
||||||
|
german = db.Column(db.String(100), nullable=False)
|
||||||
|
level = db.Column(db.Integer, nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
# User loader callback for Flask-Login
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
|
# Route Handlers
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('start.html')
|
||||||
|
|
||||||
|
@app.route('/train')
|
||||||
|
@login_required
|
||||||
|
def train():
|
||||||
|
return render_template('train.html') # Stelle sicher, dass diese Datei existiert
|
||||||
|
|
||||||
|
@app.route('/settings')
|
||||||
|
@login_required
|
||||||
|
def settings():
|
||||||
|
return render_template('settings.html') # Stelle sicher, dass diese Datei existiert
|
||||||
|
|
||||||
|
@app.route('/vocab')
|
||||||
|
@login_required
|
||||||
|
def vocab():
|
||||||
|
return render_template('vocab.html')
|
||||||
|
|
||||||
|
@app.route('/register', methods=['POST'])
|
||||||
|
def register():
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
if not username or not password:
|
||||||
|
return jsonify({'error': 'Username and password are required'}), 400
|
||||||
|
|
||||||
|
hashed_password = generate_password_hash(password, method='pbkdf2:sha256')
|
||||||
|
user = User(username=username, password=hashed_password)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 201
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
username = request.form['username']
|
||||||
|
password = request.form['password']
|
||||||
|
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if user and check_password_hash(user.password, password):
|
||||||
|
session['logged_in'] = True
|
||||||
|
session['user_id'] = user.id
|
||||||
|
return redirect('/settings')
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Invalid credentials"}), 401
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/logout', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/add_vocab', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def add_vocab():
|
||||||
|
english = request.form.get('english')
|
||||||
|
german = request.form.get('german')
|
||||||
|
level = int(request.form.get('level'))
|
||||||
|
|
||||||
|
if not english or not german or not level:
|
||||||
|
return jsonify({'error': 'All fields are required'}), 400
|
||||||
|
|
||||||
|
vocab = Vocabulary(english=english, german=german, level=level, user_id=current_user.id)
|
||||||
|
try:
|
||||||
|
db.session.add(vocab)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 201
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/update_vocab/<int:id>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_vocab(id):
|
||||||
|
vocab = Vocabulary.query.get(id)
|
||||||
|
if not vocab or vocab.user_id != current_user.id:
|
||||||
|
return jsonify({'error': 'Not found or unauthorized'}), 404
|
||||||
|
|
||||||
|
english = request.json.get('english')
|
||||||
|
german = request.json.get('german')
|
||||||
|
level = request.json.get('level')
|
||||||
|
|
||||||
|
if english:
|
||||||
|
vocab.english = english
|
||||||
|
if german:
|
||||||
|
vocab.german = german
|
||||||
|
if level:
|
||||||
|
vocab.level = level
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 200
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/delete_vocab/<int:id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_vocab(id):
|
||||||
|
vocab = Vocabulary.query.get(id)
|
||||||
|
if not vocab or vocab.user_id != current_user.id:
|
||||||
|
return jsonify({'error': 'Not found or unauthorized'}), 404
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.delete(vocab)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'success': True}), 200
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/get_vocab', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_vocab():
|
||||||
|
vocab_list = Vocabulary.query.filter_by(user_id=current_user.id).all()
|
||||||
|
result = [{'id': v.id, 'english': v.english, 'german': v.german, 'level': v.level} for v in vocab_list]
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
app.run(debug=True)
|
Loading…
x
Reference in New Issue
Block a user