Compare commits
3 commits
fcea32c419
...
dff44f40c1
Author | SHA1 | Date | |
---|---|---|---|
dff44f40c1 | |||
8bc257aec6 | |||
11760988ee |
2 changed files with 764 additions and 0 deletions
71
README.md
Normal file
71
README.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# 🗝️ Game Key Management System 🔑
|
||||||
|
|
||||||
|
![Project Logo - ideally a nerdy picture of keys or a gamepad]
|
||||||
|
|
||||||
|
**Welcome!** 👋
|
||||||
|
|
||||||
|
This project helps you keep track of your collected game keys. Does this sound familiar? You have a key here, a key there, from Humble Bundle, Fanatical, or just given to you, but you can't remember if you redeemed it, gifted it, or if it's still lurking somewhere? Don't panic, this is the solution!
|
||||||
|
|
||||||
|
## ✨ Features ✨
|
||||||
|
|
||||||
|
* **Key Management:** Enter your game keys, along with the corresponding game, platform (Steam, GOG, etc.) and where you got the key.
|
||||||
|
* **Status Tracking:** Mark keys as "Redeemed", "Gifted" or "Available". So you always know where you stand.
|
||||||
|
* **Clear Database:** All your keys in one place, easily searchable and sortable.
|
||||||
|
* **(Planned Features):**
|
||||||
|
* Import/Export of Keys (CSV, JSON)
|
||||||
|
* Integration with Steam API (to automatically check if a key has already been redeemed)
|
||||||
|
* Dark Mode! (Because who doesn't love Dark Mode?)
|
||||||
|
|
||||||
|
## 🚀 Get Started! 🚀
|
||||||
|
|
||||||
|
1. **Clone the Repository:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [Repository URL]
|
||||||
|
cd [Project Directory]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Installation (if required - e.g. for a web application):**
|
||||||
|
|
||||||
|
* Describe the necessary installation steps here. Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Or yarn install, whatever you prefer!
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configuration:**
|
||||||
|
|
||||||
|
* Explain here which configuration steps are necessary (e.g. setting up a database connection).
|
||||||
|
* Example: Create a `.env` file and enter your database access data.
|
||||||
|
|
||||||
|
4. **Start the Application:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start # Or however the application is started
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Use the Application!** Open your browser and go to `http://localhost:[Port]` (or wherever your application is running).
|
||||||
|
|
||||||
|
## 🛠️ Technology Stack 🛠️
|
||||||
|
|
||||||
|
* **Frontend:** [Enter the frontend technology here - e.g. React, Vue.js, Angular]
|
||||||
|
* **Backend:** [Enter the backend technology here - e.g. Node.js, Python/Flask, Java/Spring]
|
||||||
|
* **Database:** [Enter the database here - e.g. PostgreSQL, MySQL, SQLite]
|
||||||
|
|
||||||
|
## 🙌 Contribute! 🙌
|
||||||
|
|
||||||
|
This project is open source and thrives on your help! If you find bugs, have suggestions, or want to contribute code yourself, you are welcome!
|
||||||
|
|
||||||
|
* **Bug Reports:** Please report bugs as Issues.
|
||||||
|
* **Feature Requests:** Suggest new features!
|
||||||
|
* **Pull Requests:** Submit your code changes!
|
||||||
|
|
||||||
|
Before contributing code, please read the [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
||||||
|
|
||||||
|
## 📜 License 📜
|
||||||
|
|
||||||
|
This project is licensed under the [MIT License](LICENSE).
|
||||||
|
|
||||||
|
## 💖 Acknowledgements 💖
|
||||||
|
|
||||||
|
A big thank you to everyone who supports and contributes to this project!
|
693
setup.sh
Normal file
693
setup.sh
Normal file
|
@ -0,0 +1,693 @@
|
||||||
|
#!/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 <<EOL > 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)
|
||||||
|
steam_appid = db.Column(db.String(20))
|
||||||
|
|
||||||
|
@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/<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/<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'))
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
def extract_steam_appid(url):
|
||||||
|
match = re.search(r'store\.steampowered\.com/app/(\d+)', url or '')
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@app.route('/add', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def add_game():
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
url = request.form.get('url', '')
|
||||||
|
steam_appid = request.form.get('steam_appid', '').strip()
|
||||||
|
if not steam_appid:
|
||||||
|
steam_appid = extract_steam_appid(url)
|
||||||
|
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=url,
|
||||||
|
steam_appid=steam_appid, # <- jetzt wird sie gesetzt!
|
||||||
|
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/<int:game_id>', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def edit_game(game_id):
|
||||||
|
game = db.session.get(Game, game_id) # SQLAlchemy 2.x-kompatibel
|
||||||
|
if not game or game.owner != current_user:
|
||||||
|
return _("Not allowed!"), 403
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
# Steam AppID aus Formular oder URL extrahieren
|
||||||
|
url = request.form.get('url', '')
|
||||||
|
steam_appid = request.form.get('steam_appid', '').strip()
|
||||||
|
if not steam_appid:
|
||||||
|
steam_appid = extract_steam_appid(url)
|
||||||
|
|
||||||
|
# Aktualisiere alle Felder
|
||||||
|
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 = url
|
||||||
|
game.steam_appid = steam_appid # <- FEHLTE HIER
|
||||||
|
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/<int:game_id>', 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 <<EOL > babel.cfg
|
||||||
|
[python: **.py]
|
||||||
|
[jinja2: templates/**.html]
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# 5. Dockerfile
|
||||||
|
cat <<DOCKER_END > 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 <<COMPOSE_END > 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 <<HTML_END > templates/base.html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ get_locale() }}" data-bs-theme="{{ theme }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ _('Steam Manager') }}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="/">{{ _('Steam Manager') }}</a>
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<form class="d-flex" action="{{ url_for('index') }}" method="GET">
|
||||||
|
<input class="form-control me-2"
|
||||||
|
type="search"
|
||||||
|
name="q"
|
||||||
|
placeholder="{{ _('Search') }}"
|
||||||
|
value="{{ search_query }}">
|
||||||
|
<button class="btn btn-outline-success" type="submit">🔍</button>
|
||||||
|
</form>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="darkModeSwitch" {% if theme == 'dark' %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="darkModeSwitch">{{ _('Dark Mode') }}</label>
|
||||||
|
</div>
|
||||||
|
<!-- Sprachumschalter -->
|
||||||
|
<div class="dropdown ms-3">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{% if get_locale() == 'de' %} Deutsch {% elif get_locale() == 'en' %} English {% else %} Sprache {% endif %}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if get_locale() == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">
|
||||||
|
Deutsch
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {% if get_locale() == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">
|
||||||
|
English
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="btn btn-danger ms-3">{{ _('Logout') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="container mt-4">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }} alert-dismissible fade show">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const toggle = document.getElementById('darkModeSwitch');
|
||||||
|
const html = document.documentElement;
|
||||||
|
toggle.addEventListener('change', function() {
|
||||||
|
const theme = this.checked ? 'dark' : 'light';
|
||||||
|
fetch('/set-theme/' + theme)
|
||||||
|
.then(() => {
|
||||||
|
html.setAttribute('data-bs-theme', theme);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML_END
|
||||||
|
|
||||||
|
|
||||||
|
cat <<HTML_END > templates/index.html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>{{ _('My Games') }}</h1>
|
||||||
|
<a href="{{ url_for('add_game') }}" class="btn btn-primary">
|
||||||
|
+ {{ _('Add New Game') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if games %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>{{ _('Cover') }}</th>
|
||||||
|
<th>{{ _('Name') }}</th>
|
||||||
|
<th>{{ _('Key') }}</th>
|
||||||
|
<th>{{ _('Status') }}</th>
|
||||||
|
<th>{{ _('Created') }}</th>
|
||||||
|
<th>{{ _('Redeem by') }}</th>
|
||||||
|
<th>{{ _('Shop') }}</th>
|
||||||
|
<th>{{ _('Actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for game in games %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if game.steam_appid %}
|
||||||
|
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
|
||||||
|
alt="Steam Header" style="height:64px;max-width:120px;object-fit:cover;">
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ game.name }}</td>
|
||||||
|
<td class="font-monospace">{{ game.steam_key }}</td>
|
||||||
|
<td>
|
||||||
|
{% if game.status == 'nicht eingelöst' %}
|
||||||
|
<span class="badge bg-warning text-dark">{{ _('Not redeemed') }}</span>
|
||||||
|
{% elif game.status == 'verschenkt' %}
|
||||||
|
<span class="badge bg-success">{{ _('Gifted') }}</span>
|
||||||
|
{% elif game.status == 'eingelöst' %}
|
||||||
|
<span class="badge bg-secondary">{{ _('Redeemed') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ format_date(game.created_at) }}</td>
|
||||||
|
<td>
|
||||||
|
{% if game.redeem_date %}
|
||||||
|
<span class="badge bg-danger">{{ format_date(game.redeem_date) }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if game.url %}
|
||||||
|
<a href="{{ game.url }}" target="_blank" class="btn btn-sm btn-outline-info">🔗 {{ _('Shop') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
<a href="{{ url_for('edit_game', game_id=game.id) }}" class="btn btn-sm btn-warning">✏️</a>
|
||||||
|
<form method="POST" action="{{ url_for('delete_game', game_id=game.id) }}" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ _('Really delete?') }}')">🗑️</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">{{ _('No games yet') }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
HTML_END
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cat <<HTML_END > templates/add_game.html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card p-4 shadow-sm">
|
||||||
|
<h2 class="mb-4">{{ _('Add New Game') }}</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{{ _('Name') }} *</label>
|
||||||
|
<input type="text" name="name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{{ _('Steam Key') }} *</label>
|
||||||
|
<input type="text" name="steam_key" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{{ _('Status') }} *</label>
|
||||||
|
<select name="status" class="form-select" required>
|
||||||
|
<option value="nicht eingelöst">{{ _('Not redeemed') }}</option>
|
||||||
|
<option value="verschenkt">{{ _('Gifted') }}</option>
|
||||||
|
<option value="eingelöst">{{ _('Redeemed') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{{ _('Redeem by') }}</label>
|
||||||
|
<input type="date" name="redeem_date" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{{ _('Recipient') }}</label>
|
||||||
|
<input type="text" name="recipient" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">{{ _('Shop URL') }}</label>
|
||||||
|
<input type="url" name="url" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">{{ _('Notes') }}</label>
|
||||||
|
<textarea name="notes" class="form-control" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-success">{{ _('Save') }}</button>
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
HTML_END
|
||||||
|
|
||||||
|
|
||||||
|
cat <<HTML_END > templates/edit_game.html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card p-4 shadow-sm">
|
||||||
|
<h2 class="mb-4">{{ _('Edit Game') }}</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{{ _('Name') }} *</label>
|
||||||
|
<input type="text" name="name" class="form-control" value="{{ game.name }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{{ _('Steam Key') }} *</label>
|
||||||
|
<input type="text" name="steam_key" class="form-control" value="{{ game.steam_key }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{{ _('Steam AppID (optional)') }}</label>
|
||||||
|
<input type="text" name="steam_appid" class="form-control" value="{{ game.steam_appid or '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{{ _('Status') }} *</label>
|
||||||
|
<select name="status" class="form-select" required>
|
||||||
|
<option value="nicht eingelöst" {% if game.status == 'nicht eingelöst' %}selected{% endif %}>{{ _('Not redeemed') }}</option>
|
||||||
|
<option value="verschenkt" {% if game.status == 'verschenkt' %}selected{% endif %}>{{ _('Gifted') }}</option>
|
||||||
|
<option value="eingelöst" {% if game.status == 'eingelöst' %}selected{% endif %}>{{ _('Redeemed') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{{ _('Redeem by') }}</label>
|
||||||
|
<input type="date" name="redeem_date" class="form-control" value="{{ redeem_date }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">{{ _('Recipient') }}</label>
|
||||||
|
<input type="text" name="recipient" class="form-control" value="{{ game.recipient }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">{{ _('Shop URL') }}</label>
|
||||||
|
<input type="url" name="url" class="form-control" value="{{ game.url }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">{{ _('Notes') }}</label>
|
||||||
|
<textarea name="notes" class="form-control" rows="3">{{ game.notes }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary">{{ _('Save') }}</button>
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
HTML_END
|
||||||
|
|
||||||
|
|
||||||
|
cat <<HTML_END > templates/login.html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title mb-4">{{ _('Login') }}</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ _('Username') }}</label>
|
||||||
|
<input type="text" name="username" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ _('Password') }}</label>
|
||||||
|
<input type="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">{{ _('Login') }}</button>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<a href="{{ url_for('register') }}">{{ _('No account yet? Register') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
HTML_END
|
||||||
|
|
||||||
|
|
||||||
|
cat <<HTML_END > templates/register.html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title mb-4">{{ _('Register') }}</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ _('Username') }}</label>
|
||||||
|
<input type="text" name="username" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ _('Password') }}</label>
|
||||||
|
<input type="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">{{ _('Register') }}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
HTML_END
|
||||||
|
|
||||||
|
|
||||||
|
# CSS
|
||||||
|
cat <<CSS_END > 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"
|
Loading…
Add table
Add a link
Reference in a new issue