diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..27345e9 --- /dev/null +++ b/setup.sh @@ -0,0 +1,657 @@ +#!/bin/bash +set -e + +# Konfiguration +PROJECT_DIR="steam-gift-manager" +TRANSLATIONS_DIR="$PWD/steam-translations" +DATA_DIR="$PWD/data" + +# 1. Projektordner & Übersetzungsordner erstellen +mkdir -p $PROJECT_DIR/{templates,static} +mkdir -p $TRANSLATIONS_DIR +mkdir -p $DATA_DIR +cd $PROJECT_DIR + +# 2. requirements.txt +cat < requirements.txt +flask +flask-login +werkzeug +python-dotenv +flask-sqlalchemy +flask-babel +jinja2<3.1.0 +EOL + +# 3. app.py (angepasst für SQLAlchemy 2.x) +cat <<'PYTHON_END' > app.py +from flask import Flask, render_template, request, redirect, url_for, flash, make_response, session, abort +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user +from flask_babel import Babel, _ +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime +import os + +app = Flask(__name__) +app.config['SECRET_KEY'] = os.urandom(24) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/data/games.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['BABEL_DEFAULT_LOCALE'] = 'de' +app.config['BABEL_SUPPORTED_LOCALES'] = ['de', 'en'] +app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations' + +db = SQLAlchemy(app) +login_manager = LoginManager(app) +login_manager.login_view = 'login' +babel = Babel(app) + +@babel.localeselector +def get_locale(): + if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']: + return session['lang'] + return request.accept_languages.best_match(app.config['BABEL_SUPPORTED_LOCALES']) + +@app.context_processor +def inject_template_vars(): + return dict( + get_locale=get_locale, + theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light' + ) + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100), unique=True) + password = db.Column(db.String(100)) + games = db.relationship('Game', backref='owner', lazy=True) + +class Game(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + steam_key = db.Column(db.String(100), nullable=False) + status = db.Column(db.String(50), nullable=False) + recipient = db.Column(db.String(100)) + notes = db.Column(db.Text) + url = db.Column(db.String(200)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + redeem_date = db.Column(db.DateTime) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + +@login_manager.user_loader +def load_user(user_id): + return db.session.get(User, int(user_id)) + +@app.route('/') +@login_required +def index(): + search_query = request.args.get('q', '') + query = db.session.query(Game).filter_by(user_id=current_user.id) + if search_query: + query = query.filter(Game.name.ilike(f'%{search_query}%')) + games = query.order_by(Game.created_at.desc()).all() + return render_template('index.html', + games=games, + format_date=lambda dt: dt.strftime('%d.%m.%Y') if dt else '', + search_query=search_query) + +@app.route('/set-lang/') +def set_lang(lang): + if lang in app.config['BABEL_SUPPORTED_LOCALES']: + session['lang'] = lang + return redirect(request.referrer or url_for('index')) + +@app.route('/set-theme/') +def set_theme(theme): + resp = make_response('', 204) + resp.set_cookie('dark_mode', 'true' if theme == 'dark' else 'false', max_age=60*60*24*365) + return resp + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + 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): + login_user(user) + return redirect(url_for('index')) + flash(_('Invalid credentials'), 'danger') + return render_template('login.html') + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + username = request.form['username'] + password = generate_password_hash(request.form['password']) + if User.query.filter_by(username=username).first(): + flash(_('Username already exists'), 'danger') + return redirect(url_for('register')) + new_user = User(username=username, password=password) + db.session.add(new_user) + db.session.commit() + login_user(new_user) + return redirect(url_for('index')) + return render_template('register.html') + +@app.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('login')) + +@app.route('/add', methods=['GET', 'POST']) +@login_required +def add_game(): + if request.method == 'POST': + try: + new_game = Game( + name=request.form['name'], + steam_key=request.form['steam_key'], + status=request.form['status'], + recipient=request.form.get('recipient', ''), + notes=request.form.get('notes', ''), + url=request.form.get('url', ''), + redeem_date=datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None, + user_id=current_user.id + ) + db.session.add(new_game) + db.session.commit() + flash(_('Game added successfully!'), 'success') + return redirect(url_for('index')) + except Exception as e: + db.session.rollback() + flash(_('Error: ') + str(e), 'danger') + return render_template('add_game.html') + +@app.route('/edit/', methods=['GET', 'POST']) +@login_required +def edit_game(game_id): + game = Game.query.get_or_404(game_id) + if game.owner != current_user: + return _("Not allowed!"), 403 + if request.method == 'POST': + try: + game.name = request.form['name'] + game.steam_key = request.form['steam_key'] + game.status = request.form['status'] + game.recipient = request.form.get('recipient', '') + game.notes = request.form.get('notes', '') + game.url = request.form.get('url', '') + game.redeem_date = datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None + db.session.commit() + flash(_('Changes saved!'), 'success') + return redirect(url_for('index')) + except Exception as e: + db.session.rollback() + flash(_('Error: ') + str(e), 'danger') + return render_template('edit_game.html', + game=game, + redeem_date=game.redeem_date.strftime('%Y-%m-%d') if game.redeem_date else '') + +@app.route('/delete/', methods=['POST']) +@login_required +def delete_game(game_id): + game = Game.query.get_or_404(game_id) + if game.owner != current_user: + return _("Not allowed!"), 403 + try: + db.session.delete(game) + db.session.commit() + flash(_('Game deleted!'), 'success') + except Exception as e: + db.session.rollback() + flash(_('Error deleting: ') + str(e), 'danger') + return redirect(url_for('index')) + + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + app.run(host='0.0.0.0', port=5000) +PYTHON_END + +# 4. babel.cfg +cat < babel.cfg +[python: **.py] +[jinja2: templates/**.html] +EOL + +# 5. Dockerfile +cat < Dockerfile +FROM python:3.10-slim + +# Shell explizit setzen +SHELL ["/bin/bash", "-c"] + +# Datenbankordner erstellen und Berechtigungen setzen +RUN mkdir -p /app/data && chmod -R a+rwX /app/data + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ARG UID=1000 +ARG GID=1000 + +RUN groupadd -g \$GID appuser && \ + useradd -u \$UID -g \$GID -m appuser && \ + chown -R appuser:appuser /app + +USER appuser + +EXPOSE 5000 +CMD ["python", "app.py"] +DOCKER_END + +# 6. docker-compose.yml +cat < docker-compose.yml +version: '3.8' + +services: + steam-manager: + build: . + ports: + - "5000:5000" + volumes: + - $DATA_DIR:/app/data + - $TRANSLATIONS_DIR:/app/translations + environment: + - FLASK_DEBUG=0 + restart: unless-stopped +COMPOSE_END + +# 7. Verzeichnisse und Berechtigungen +mkdir -p ../data ../steam-translations +chmod -R a+rwX ../data ../steam-translations + +# 7. Übersetzungs-Workflow-Script +cat <<'SCRIPT_END' > ../translate.sh +#!/bin/bash +set -e + +cd "$(dirname "$0")/steam-gift-manager" + +# 1. Extrahiere alle Texte +docker-compose exec steam-manager pybabel extract -F babel.cfg -o translations/messages.pot . + +# 2. Initialisiere Sprachen (nur einmal nötig, danach auskommentieren) +for lang in de en; do + if [ ! -f "../steam-translations/$lang/LC_MESSAGES/messages.po" ]; then + docker-compose exec steam-manager pybabel init -i translations/messages.pot -d translations -l $lang + fi +done + +# 3. Aktualisiere Übersetzungen +docker-compose exec steam-manager pybabel update -i translations/messages.pot -d translations + +# 4. Kompiliere Übersetzungen +docker-compose exec steam-manager pybabel compile -d translations + +echo "✅ Übersetzungen extrahiert, aktualisiert und kompiliert!" +SCRIPT_END +chmod +x ../translate.sh + + + +cat < templates/base.html + + + + + + {{ _('Steam Manager') }} + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + + + +HTML_END + + +cat < templates/index.html +{% extends "base.html" %} +{% block content %} +
+

{{ _('My Games') }}

+ + + {{ _('Add New Game') }} + +
+ +{% if games %} +
+ + + + + + + + + + + + + + {% for game in games %} + + + + + + + + + + {% endfor %} + +
{{ _('Name') }}{{ _('Key') }}{{ _('Status') }}{{ _('Created') }}{{ _('Redeem by') }}{{ _('Shop') }}{{ _('Actions') }}
{{ game.name }}{{ game.steam_key }} + {% if game.status == 'nicht eingelöst' %} + {{ _('Not redeemed') }} + {% elif game.status == 'verschenkt' %} + {{ _('Gifted') }} + {% elif game.status == 'eingelöst' %} + {{ _('Redeemed') }} + {% endif %} + {{ format_date(game.created_at) }} + {% if game.redeem_date %} + {{ format_date(game.redeem_date) }} + {% endif %} + + {% if game.url %} + 🔗 {{ _('Shop') }} + {% endif %} + + ✏️ +
+ +
+
+
+{% else %} +
{{ _('No games yet') }}
+{% endif %} +{% endblock %} +HTML_END + + +cat < templates/add_game.html +{% extends "base.html" %} +{% block content %} +
+

{{ _('Add New Game') }}

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + {{ _('Cancel') }} +
+
+
+
+{% endblock %} +HTML_END + + +cat < templates/edit_game.html +{% extends "base.html" %} +{% block content %} +
+

{{ _('Edit Game') }}

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + {{ _('Cancel') }} +
+
+
+
+{% endblock %} +HTML_END + + +cat < templates/login.html +{% extends "base.html" %} +{% block content %} +
+
+
+
+

{{ _('Login') }}

+
+
+ + +
+
+ + +
+ +
+ +
+
+
+
+{% endblock %} +HTML_END + + +cat < templates/register.html +{% extends "base.html" %} +{% block content %} +
+
+
+
+

{{ _('Register') }}

+
+
+ + +
+
+ + +
+ +
+
+
+
+
+{% endblock %} +HTML_END + + +# CSS +cat < static/style.css +:root { + --bs-body-bg: #ffffff; + --bs-body-color: #212529; +} +[data-bs-theme="dark"] { + --bs-body-bg: #1a1a1a; + --bs-body-color: #f8f9fa; + --bs-border-color: #495057; +} +[data-bs-theme="dark"] .table { + --bs-table-bg: #212529; + --bs-table-color: #fff; + --bs-table-border-color: #495057; +} +[data-bs-theme="dark"] .card { + background-color: #2b3035; + border-color: var(--bs-border-color); +} +[data-bs-theme="dark"] .navbar { + background-color: #212529 !important; +} +body { + background-color: var(--bs-body-bg); + color: var(--bs-body-color); + transition: all 0.3s ease; +} +.font-monospace { + font-family: Monaco, Consolas, "Courier New", monospace; +} +.badge { + font-size: 0.9em; + font-weight: 500; +} +CSS_END + + +echo -e "\n\033[1;32m✅ Setup abgeschlossen!\033[0m" +echo -e "Starten mit:" +echo -e "cd steam-gift-manager" +echo -e "docker-compose build --no-cache && docker-compose up -d" +echo -e "\n\033[1;34mÜbersetzungen initialisieren, bearbeiten und kompilieren:\033[0m" +echo -e "1. ./translate.sh" +echo -e "2. Bearbeite die .po-Dateien in $TRANSLATIONS_DIR/de/LC_MESSAGES/messages.po und .../en/LC_MESSAGES/messages.po" +echo -e "3. ./translate.sh erneut ausführen und Container neustarten, damit Änderungen aktiv werden"