Version 1.0

This commit is contained in:
nocci 2025-04-26 14:32:07 +02:00
parent 4bebbb27e4
commit eaa508a8df
33 changed files with 2822 additions and 994 deletions

34
.env Normal file
View File

@ -0,0 +1,34 @@
# Flask-Configuration
SECRET_KEY="e2460fd1be9778d6a17aaa7e98676b042936856d70c3f5b2"
REDEEM_SECRET="590ba02d77e340284e48075fee6ff692"
WTF_CSRF_SECRET_KEY="2377b17a84388b7c9ff2fce049dc1898"
# locales
BABEL_DEFAULT_LOCALE="en"
BABEL_SUPPORTED_LOCALES="de,en"
BABEL_TRANSLATION_DIRECTORIES="translations"
# Timezone
TZ=Europe/Berlin
# Security
SESSION_COOKIE_SECURE="False"
CSRF_ENABLED="True"
# Account registration
REGISTRATION_ENABLED="True"
# checking interval if keys have to be redeemed before a specific date
CHECK_EXPIRING_KEYS_INTERVAL_HOURS=6
# Pushover
PUSHOVER_APP_TOKEN=""
PUSHOVER_USER_KEY=""
# Gotify
GOTIFY_URL=""
GOTIFY_TOKEN=""
# Matrix
MATRIX_HOMESERVER=""
MATRIX_ACCESS_TOKEN=""
MATRIX_ROOM_ID=""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

@ -1,34 +1,50 @@
# 🔑 Game Key Management System 🔑
# 🔑 Game Key Manager 🔑
![Screenshot](GameManager.png)
## Welcome! 👋
## 👋 Welcome! 👋
This project helps you keep track of your collected game keys.
No more confusion about whether a key is redeemed, gifted, or still unused now you have everything in one place, with search, status, and even automatic Steam cover images!
It's even possible to gift your keys via a unique website. Just edit the game to "Gifted" and you'll get a option to copy the on your overview page. (maybe HTTPS only)
(the link will also remain in the edit area)
![Screenshot](GameManager.png)
---
## ✨ Features ✨
- **Key Management:**
Enter your game keys, the corresponding game, platform, and where you got the key.
Enter your game keys, the corresponding game, platform, and maybe where you got the key.
- **Status Tracking:**
Mark keys as "Redeemed", "Gifted" or "Available" always know your status.
- **Shop URL & Steam Cover:**
Save the shop URL and (optionally) the Steam AppID. The app will automatically show the official Steam cover image if available.
- **Gift your Games:**
You can create a unique redeem/gift website, which will expire after 24h.
- **Multi-user:**
Each user manages their own keys.
- **Search & Filter:**
- **Enable/Disable Registrations:**
Perfect if you want to run the Server just on your own (via .env file)
- **Search:**
Find games quickly with the search function.
- **Responsive UI:**
Works on desktop and mobile, with Dark Mode toggle.
- **Multi-language:**
Switch between English and German instantly.
Switch between English and German instantly*.
- **Import/Export (CSV / PDF -only export-):**
Easy export and import of your keys. (e.g. in case you have to start over)
- **Change Password:**
Change your Password on the fly.
- **Website Security:**
You can turn on/off CSRF and Secure Cookie via .env file.
- **Notifications:**
If you have key that have to be redeemed before a specific date. You can set up sending messages via, Pushover, Matrix and Gotify
- **No key data leaves your server!**
- **(Planned):**
- Import/Export (CSV, JSON)
- Redeem site with unique sharing link
- ~~Import/Export (CSV)~~
- ~~Redeem site with unique sharing link~~
---
@ -37,13 +53,14 @@ No more confusion about whether a key is redeemed, gifted, or still unused n
### 1. **Clone the Repository**
```bash
git clone https://git.nocci.it/nocci/GiftGamesDB
git clone https://git.nocci.it/nocci/GameKeyManager
cd steam-gift-manager
```
### 2. **Setup Docker**
Make sure you have [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) installed.
If not, the script will ask you what to do and can install Docker and docker-compose for you. (maybe not if you are running Arch)
### 3. **Initial Setup**
@ -62,54 +79,90 @@ docker-compose build --no-cache
docker-compose up -d
```
### 5. **Initialize and Edit Translations (Optional)**
### 5. **Edit your .env file to your liking**
It's in your root folder of the installation!
```xml
# Sicherheit
SESSION_COOKIE_SECURE="True" (only works if you run this app via HTTPS)
CSRF_ENABLED="True"
```
**Important after any(!) change of the .env file!**
```bash
cd steam-gift-manager/
docker-compose down && docker-compose up -d --build
```
### 6. **Initialize and Edit Translations (Optional)**
```bash
./translate.sh
```
Edit the .po files in steam-translations/de/LC_MESSAGES/messages.po and en/LC_MESSAGES/messages.po
Edit the .po files in translations/de_DE/LC_MESSAGES/messages.po and en_US/LC_MESSAGES/messages.po
```bash
./translate.sh
cd steam-gift-manager/
docker-compose restart steam-manager
docker-compose down && docker-compose up -d --build
```
### 6. **Open the App**
### 7. **Open the App**
Go to [http://localhost:5000](http://localhost:5000) in your browser.
- Register your first user.
- Add your keys, shop URLs, and (optionally) Steam AppIDs.
- Add your keys, shop URLs etc.
- Enjoy search, status, and automatic Steam cover images!
---
## 🛠️ Technology Stack 🛠️
- **Frontend:** Bootstrap 5, Jinja2 Templates
- **Backend:** Python 3, Flask, Flask-Babel, Flask-Login, Flask-SQLAlchemy
- **Frontend:** Bootstrap 5, Jinja2 Templates ...
- **Backend:** Python 3, Flask, Flask-Babel, Flask-Login, Flask-SQLAlchemy ...
- **Database:** SQLite (persisted in `data/`)
- **Containerization:** Docker, docker-compose
- **Translations:** Flask-Babel, editable `.po` files in `steam-translations/`
- **Translations:** Flask-Babel, editable `.po` files in `translations/`
## 🌍 Multi-language
- Switch between English and German using the dropdown in the navigation bar.
- All game and menu texts are translated.
- You can add more languages by editing the `.po` files and running `./translate.sh`.
- All game and menu texts can be translated or individualized.
## 🔔 Notifications
- Send notifications if a game has to be redeemed by a specific date
- Gotify, Matrix and Pushover are already available - have a look into the .env file
- 48 hours before you are running out of time the app will send you a notice
---
## 🪙 Do you this project? 🪙
If youd like to support itme, you can make a donation here:
[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/nocci)
[![Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/nocci/donate)
Thank you!
---
## 🙌 Contribute! 🙌
This project is open source and thrives on your help!
This project is open source!
- **Bug Reports:** Please report bugs as Issues.
- **Feature Requests:** Suggest new features!
- **Pull Requests:** Submit your code changes!
// **only possible after Forgejo opens for federation** \\\
---
## 📜 License 📜
@ -124,4 +177,4 @@ A big thank you to everyone who supports and contributes to this project!
---
**Enjoy your organized Steam key collection!** 🚀
**Enjoy your organized Game key collection!** 🚀

1277
setup.sh

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,13 @@ FROM python:3.10-slim
SHELL ["/bin/bash", "-c"]
RUN mkdir -p /app/data && chmod -R a+rwX /app/data
RUN apt-get update && apt-get install -y --no-install-recommends wget && mkdir -p /app/static && wget -O /app/static/logo.png "https://git.nocci.it/nocci/GiftGamesDB/raw/branch/main/steam-gift-manager/static/logo.png" && wget -O /app/static/logo_small.png "https://git.nocci.it/nocci/GiftGamesDB/raw/branch/main/steam-gift-manager/static/logo_small.png" && wget -O /app/static/forgejo.svg "https://git.nocci.it/nocci/GiftGamesDB/raw/branch/main/steam-gift-manager/static/forgejo.svg" && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/data && chown -R 1000:1000 /app/data
ENV TZ=
RUN ln -snf /usr/share/zoneinfo/ /etc/localtime && echo > /etc/timezone
WORKDIR /app
COPY requirements.txt .

View File

@ -1,26 +1,90 @@
from flask import Flask, render_template, request, redirect, url_for, flash, make_response, session, abort, send_file
import os
import logging
import warnings
from sqlalchemy.exc import LegacyAPIWarning
warnings.simplefilter("ignore", category=LegacyAPIWarning)
from flask import Flask, render_template, request, redirect, url_for, flash, make_response, session, abort, send_file, jsonify
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
from datetime import datetime, timedelta
from flask_wtf import CSRFProtect
from flask import abort
import io
import warnings
import re
import io
import csv
import secrets
import requests
from dotenv import load_dotenv
load_dotenv(override=True)
from sqlalchemy.exc import IntegrityError
from apscheduler.schedulers.background import BackgroundScheduler
import atexit
from flask_migrate import Migrate
from sqlalchemy import MetaData
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4, landscape, letter
from reportlab.platypus import (
SimpleDocTemplate,
Table,
TableStyle,
Paragraph,
Image,
Spacer
)
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.utils import ImageReader
from reportlab.lib.units import cm, inch, mm
from io import BytesIO
import reportlab.lib
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'
csrf = CSRFProtect(app)
db = SQLAlchemy(app)
convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s"
}
metadata = MetaData(naming_convention=convention)
load_dotenv(override=True)
# Lade Umgebungsvariablen aus .env mit override
load_dotenv(override=True)
# Konfiguration
app.config.update(
SECRET_KEY=os.getenv('SECRET_KEY'),
SQLALCHEMY_DATABASE_URI=('sqlite:////app/data/games.db'),
SQLALCHEMY_TRACK_MODIFICATIONS=False,
BABEL_DEFAULT_LOCALE=os.getenv('BABEL_DEFAULT_LOCALE'),
BABEL_SUPPORTED_LOCALES=os.getenv('BABEL_SUPPORTED_LOCALES').split(','),
BABEL_TRANSLATION_DIRECTORIES=os.getenv('BABEL_TRANSLATION_DIRECTORIES'),
SESSION_COOKIE_SECURE=os.getenv('SESSION_COOKIE_SECURE') == 'True',
WTF_CSRF_ENABLED=os.getenv('CSRF_ENABLED') == 'True',
REGISTRATION_ENABLED=os.getenv('REGISTRATION_ENABLED', 'True').lower() == 'true'
)
interval_hours = int(os.getenv('CHECK_EXPIRING_KEYS_INTERVAL_HOURS', 12))
# Initialisierung
db = SQLAlchemy(app, metadata=metadata)
migrate = Migrate(app, db)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
babel = Babel(app)
# Logging
app.logger.addHandler(logging.StreamHandler())
app.logger.setLevel(logging.INFO)
@babel.localeselector
def get_locale():
if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']:
@ -34,36 +98,63 @@ def inject_template_vars():
theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light'
)
class User(UserMixin, db.Model):
# Datenbankmodelle
class User(db.Model, UserMixin):
__tablename__ = 'users'
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)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(256), nullable=False)
games = db.relationship('Game', back_populates='owner', lazy=True)
class Game(db.Model):
id = db.Column(db.Integer, primary_key=True)
owner = db.relationship('User', back_populates='games')
name = db.Column(db.String(100), nullable=False)
steam_key = db.Column(db.String(100), nullable=False)
steam_key = db.Column(db.String(100), nullable=False, unique=True)
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)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
steam_appid = db.Column(db.String(20))
class RedeemToken(db.Model):
id = db.Column(db.Integer, primary_key=True)
token = db.Column(db.String(17), unique=True, nullable=False)
game_id = db.Column(db.Integer, db.ForeignKey('game.id'), nullable=False)
expires = db.Column(db.DateTime, nullable=False)
used = db.Column(db.Boolean, default=False)
total_hours = db.Column(db.Integer, nullable=False)
with app.app_context():
db.create_all()
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
def extract_steam_appid(url):
match = re.search(r'store\.steampowered\.com/app/(\d+)', url or '')
return match.group(1) if match else ''
# 404
def get_or_404(model, id):
instance = db.session.get(model, id)
if not instance:
abort(404)
return instance
@app.route('/')
@login_required
def index():
search_query = request.args.get('q', '')
query = db.session.query(Game).filter_by(user_id=current_user.id)
query = Game.query.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,
@ -88,25 +179,34 @@ 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):
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 not app.config['REGISTRATION_ENABLED']:
flash(_('Registrierungen sind deaktiviert'), 'danger')
return redirect(url_for('login'))
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')
@ -115,13 +215,28 @@ def logout():
logout_user()
return redirect(url_for('login'))
import re
@app.route('/change-password', methods=['GET', 'POST'])
@login_required
def change_password():
if request.method == 'POST':
current_password = request.form['current_password']
new_password = request.form['new_password']
confirm_password = request.form['confirm_password']
def extract_steam_appid(url):
match = re.search(r'store\.steampowered\.com/app/(\d+)', url or '')
if match:
return match.group(1)
return ''
if not check_password_hash(current_user.password, current_password):
flash(_('Aktuelles Passwort ist falsch'), 'danger')
return redirect(url_for('change_password'))
if new_password != confirm_password:
flash(_('Neue Passwörter stimmen nicht überein'), 'danger')
return redirect(url_for('change_password'))
current_user.password = generate_password_hash(new_password)
db.session.commit()
flash(_('Passwort erfolgreich geändert'), 'success')
return redirect(url_for('index'))
return render_template('change_password.html')
@app.route('/add', methods=['GET', 'POST'])
@login_required
@ -130,8 +245,10 @@ def add_game():
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'],
@ -143,13 +260,19 @@ def add_game():
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 IntegrityError:
db.session.rollback()
flash(_('Steam Key already exists!'), 'danger')
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'])
@ -157,14 +280,26 @@ def add_game():
def edit_game(game_id):
game = db.session.get(Game, game_id)
if not game or game.owner != current_user:
return _("Not allowed!"), 403
abort(404)
if not game or game.owner != current_user:
abort(403)
active_redeem = RedeemToken.query.filter(
RedeemToken.game_id == game_id,
RedeemToken.expires > datetime.utcnow()
).first()
redeem_url = url_for('redeem_page', token=active_redeem.token, _external=True) if active_redeem else None
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)
game.name = request.form['name']
game.steam_key = request.form['steam_key']
game.status = request.form['status']
@ -173,32 +308,40 @@ def edit_game(game_id):
game.url = url
game.steam_appid = steam_appid
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 '')
game=game,
redeem_url=redeem_url,
active_redeem=active_redeem,
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)
game = db.session.get(Game, game_id)
if not game or game.owner != current_user:
abort(404)
if game.owner != current_user:
return _("Not allowed!"), 403
abort(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'))
# --- Import/Export Funktionen ---
@app.route('/export', methods=['GET'])
@login_required
@ -206,14 +349,22 @@ def export_games():
games = Game.query.filter_by(user_id=current_user.id).all()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['Name', 'Steam Key', 'Status', 'Recipient', 'Notes', 'URL', 'Created', 'Redeem by', 'Steam AppID'])
for game in games:
writer.writerow([
game.name, game.steam_key, game.status, game.recipient, game.notes,
game.url, game.created_at.strftime('%Y-%m-%d %H:%M:%S') if game.created_at else '',
game.name,
game.steam_key,
game.status,
game.recipient,
game.notes,
game.url,
game.created_at.strftime('%Y-%m-%d %H:%M:%S') if game.created_at else '',
game.redeem_date.strftime('%Y-%m-%d') if game.redeem_date else '',
game.steam_appid
])
output.seek(0)
return send_file(
io.BytesIO(output.getvalue().encode('utf-8')),
@ -222,35 +373,306 @@ def export_games():
download_name='games_export.csv'
)
@app.route('/export_pdf')
@login_required
def export_pdf():
excluded_statuses = ['eingelöst', 'verschenkt']
games = Game.query.filter(
Game.user_id == current_user.id,
Game.status.notin_(excluded_statuses)
).order_by(Game.created_at.desc()).all()
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer,
pagesize=landscape(A4),
leftMargin=40,
rightMargin=40,
topMargin=40,
bottomMargin=40
)
styles = getSampleStyleSheet()
elements = []
img_height = 2*cm
# Titel
elements.append(Paragraph(_("Game List (without Keys)"), styles['Title']))
elements.append(Spacer(1, 12))
# Tabellenkopf
col_widths = [
5*cm, 10*cm, 6*cm, 3*cm
]
data = [[
Paragraph('<b>Cover</b>', styles['Normal']),
Paragraph('<b>Name</b>', styles['Normal']),
Paragraph('<b>Shop-Link</b>', styles['Normal']),
Paragraph('<b>Einlösen bis</b>', styles['Normal'])
]]
for game in games:
img = None
if game.steam_appid:
try:
img_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{game.steam_appid}/header.jpg"
img_data = io.BytesIO(requests.get(img_url, timeout=5).content)
img = Image(img_data, width=3*cm, height=img_height)
except Exception:
img = Paragraph('', styles['Normal'])
data.append([
img or '',
Paragraph(game.name, styles['Normal']),
Paragraph(game.url or '', styles['Normal']),
game.redeem_date.strftime('%d.%m.%y') if game.redeem_date else ''
])
# Tabelle formatieren
table = Table(data, colWidths=col_widths, repeatRows=1)
table.setStyle(TableStyle([
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,0), 8),
('FONTSIZE', (0,1), (-1,-1), 8),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('ALIGN', (0,0), (-1,-1), 'LEFT'),
('GRID', (0,0), (-1,-1), 0.5, colors.lightgrey),
('WORDWRAP', (1,1), (1,-1), 'CJK'),
]))
elements.append(table)
doc.build(elements)
buffer.seek(0)
return send_file(
buffer,
mimetype='application/pdf',
as_attachment=True,
download_name=f'game_export_{datetime.now().strftime("%Y%m%d")}.pdf'
)
@app.route('/import', methods=['GET', 'POST'])
@login_required
def import_games():
if request.method == 'POST':
file = request.files.get('file')
if file and file.filename.endswith('.csv'):
stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None)
stream = io.StringIO(file.stream.read().decode("UTF8"))
reader = csv.DictReader(stream)
for row in reader:
new_game = Game(
name=row['Name'],
steam_key=row['Steam Key'],
status=row['Status'],
recipient=row.get('Recipient', ''),
notes=row.get('Notes', ''),
url=row.get('URL', ''),
created_at=datetime.strptime(row['Created'], '%Y-%m-%d %H:%M:%S') if row.get('Created') else datetime.utcnow(),
redeem_date=datetime.strptime(row['Redeem by'], '%Y-%m-%d') if row.get('Redeem by') else None,
steam_appid=row.get('Steam AppID', ''),
user_id=current_user.id
)
db.session.add(new_game)
db.session.commit()
flash(_('Import erfolgreich!'), 'success')
new_games = 0
duplicates = 0
try:
with db.session.begin_nested():
for row in reader:
steam_key = row['Steam Key'].strip()
if Game.query.filter_by(steam_key=steam_key).first():
duplicates += 1
continue
game = Game(
name=row['Name'],
steam_key=steam_key,
status=row['Status'],
recipient=row.get('Recipient', ''),
notes=row.get('Notes', ''),
url=row.get('URL', ''),
created_at=datetime.strptime(row['Created'], '%Y-%m-%d %H:%M:%S') if row.get('Created') else datetime.utcnow(),
redeem_date=datetime.strptime(row['Redeem by'], '%Y-%m-%d') if row.get('Redeem by') else None,
steam_appid=row.get('Steam AppID', ''),
user_id=current_user.id
)
db.session.add(game)
new_games += 1
db.session.commit()
flash(_('%(new)d neue Spiele importiert, %(dup)d Duplikate übersprungen', new=new_games, dup=duplicates), 'success')
except Exception as e:
db.session.rollback()
flash(_('Importfehler: %(error)s', error=str(e)), 'danger')
return redirect(url_for('index'))
flash(_('Bitte eine gültige CSV-Datei hochladen.'), 'danger')
return render_template('import.html')
@app.route('/generate_redeem/<int:game_id>', methods=['POST'])
@login_required
def generate_redeem(game_id):
game = db.session.get(Game, game_id)
if not game or game.owner != current_user:
abort(403)
if game.owner != current_user or game.status != 'verschenkt':
abort(403)
try:
token = secrets.token_urlsafe(12)[:17]
expires = datetime.utcnow() + timedelta(hours=24)
total_hours = 24
RedeemToken.query.filter_by(game_id=game_id).delete()
new_token = RedeemToken(
token=token,
game_id=game_id,
expires=expires,
total_hours=24
)
db.session.add(new_token)
db.session.commit()
redeem_url = url_for('redeem_page', token=token, _external=True)
return jsonify({'url': redeem_url})
except Exception as e:
app.logger.error(f"Redeem error: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/redeem/<token>')
def redeem_page(token):
redeem_token = RedeemToken.query.filter_by(token=token).first()
if not redeem_token:
abort(404)
if redeem_token.expires < datetime.utcnow():
db.session.delete(redeem_token)
db.session.commit()
abort(404)
game = Game.query.get(redeem_token.game_id)
redeem_token.used = True
db.session.commit()
return render_template('redeem.html',
game=game,
redeem_token=redeem_token,
platform_link='https://store.steampowered.com/account/registerkey?key=' if game.steam_appid else 'https://www.gog.com/redeem')
# Benachrichtigungsfunktionen
def send_pushover_notification(user, game):
"""Sendet Pushover-Benachrichtigung für ablaufenden Key"""
if not app.config['PUSHOVER_APP_TOKEN'] or not app.config['PUSHOVER_USER_KEY']:
return False
payload = {
"token": os.getenv('PUSHOVER_APP_TOKEN'),
"user": os.getenv('PUSHOVER_USER_KEY'),
"title": "Steam-Key läuft ab!",
"message": f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!",
"url": url_for('edit_game', game_id=game.id, _external=True),
"url_title": "Zum Spiel",
"priority": 1
}
try:
response = requests.post(
'https://api.pushover.net/1/messages.json',
data=payload
)
return response.status_code == 200
except Exception as e:
app.logger.error(f"Pushover error: {str(e)}")
return False
def send_gotify_notification(user, game):
"""Sendet Gotify-Benachrichtigung für ablaufenden Key"""
if not GOTIFY_URL or not GOTIFY_TOKEN:
return False
payload = {
"title": "Steam-Key läuft ab!",
"message": f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!",
"priority": 5
}
try:
response = requests.post(
f"{GOTIFY_URL}/message?token={GOTIFY_TOKEN}",
json=payload
)
return response.status_code == 200
except Exception as e:
app.logger.error(f"Gotify error: {str(e)}")
return False
def send_matrix_notification(user, game):
"""Sendet Matrix-Benachrichtigung für ablaufenden Key"""
if not MATRIX_HOMESERVER or not MATRIX_ACCESS_TOKEN or not MATRIX_ROOM_ID:
return False
try:
from matrix_client.client import MatrixClient
client = MatrixClient(MATRIX_HOMESERVER, token=MATRIX_ACCESS_TOKEN)
room = client.join_room(MATRIX_ROOM_ID)
message = f"🎮 Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!"
room.send_text(message)
return True
except Exception as e:
app.logger.error(f"Matrix error: {str(e)}")
return False
def send_notification(user, game):
"""Sendet Benachrichtigung über den bevorzugten Dienst des Benutzers"""
if user.notification_service == 'pushover':
return send_pushover_notification(user, game)
elif user.notification_service == 'gotify':
return send_gotify_notification(user, game)
elif user.notification_service == 'matrix':
return send_matrix_notification(user, game)
return False
def check_expiring_keys():
with app.app_context():
now = datetime.utcnow()
expiry_threshold = now + timedelta(hours=48)
# Moderner Select-Aufruf
stmt = select(Game).where(
Game.status != 'eingelöst',
Game.redeem_date <= expiry_threshold,
Game.redeem_date > now
)
expiring_games = db.session.execute(stmt).scalars().all()
for game in expiring_games:
user = User.query.get(game.user_id)
if user.notification_service and user.notification_service != 'none':
send_notification(user, game)
# Optional: Cleanup-Funktion für regelmäßiges Löschen abgelaufener Tokens
def cleanup_expired_tokens():
now = datetime.utcnow()
expired = RedeemToken.query.filter(RedeemToken.expires < now).all()
for token in expired:
db.session.delete(token)
db.session.commit()
# Scheduler initialisieren und starten
scheduler = BackgroundScheduler()
scheduler.add_job(func=check_expiring_keys, trigger="interval", hours=interval_hours)
scheduler.add_job(func=cleanup_expired_tokens, trigger="interval", hours=1)
scheduler.start()
# Shutdown des Schedulers bei Beendigung der App
atexit.register(lambda: scheduler.shutdown())
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(host='0.0.0.0', port=5000)

View File

@ -1,2 +1,3 @@
[python: **.py]
[jinja2: templates/**.html]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View File

@ -1,13 +1,14 @@
version: '3.8'
services:
steam-manager:
build: .
ports:
- "5000:5000"
volumes:
- /root/test/data:/app/data
- /root/test/steam-translations:/app/translations
environment:
- FLASK_DEBUG=0
- REGISTRATION_ENABLED=True
- TZ=
volumes:
- ../data:/app/data
- ../translations:/app/translations
- ../.env:/app/.env
user: "1000:1000"
restart: unless-stopped

View File

@ -1,7 +1,16 @@
flask
flask-login
flask-wtf
flask-migrate
werkzeug
python-dotenv
flask-sqlalchemy
flask-babel
jinja2<3.1.0
itsdangerous
sqlalchemy
apscheduler
matrix-client
reportlab
requests
pillow

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 212 212" width="32" height="32"><style>circle,path{fill:none;stroke:#000;stroke-width:15}path{stroke-width:25}.orange{stroke:#f60}.red{stroke:#d40000}</style><g transform="translate(6 6)"><path d="M58 168V70a50 50 0 0 1 50-50h20" class="orange"/><path d="M58 168v-30a50 50 0 0 1 50-50h20" class="red"/><circle cx="142" cy="20" r="18" class="orange"/><circle cx="142" cy="88" r="18" class="red"/><circle cx="58" cy="180" r="18" class="red"/></g></svg>

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -31,3 +31,32 @@ body {
font-size: 0.9em;
font-weight: 500;
}
#expiry-countdown {
font-weight: 600;
letter-spacing: 0.05em;
color: #dc3545;
transition: color 0.3s ease;
}
[data-bs-theme="dark"] #expiry-countdown {
color: #ff6b6b;
}
/* Progressbar-Animationen */
#expiry-bar {
transition: width 1s linear, background-color 0.5s ease;
}
.bg-success { background-color: #198754 !important; }
.bg-warning { background-color: #ffc107 !important; }
.bg-danger { background-color: #dc3545 !important; }
.progress-bar {
transition: width 1s linear, background-color 0.3s ease;
}
.table-pdf {
font-size: 0.8em;
}
.table-pdf td, .table-pdf th {
padding: 4px 8px;
}

View File

@ -3,13 +3,14 @@
<div class="card p-4 shadow-sm">
<h2 class="mb-4">{{ _('Add New Game') }}</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<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>
<label class="form-label">{{ _('Game Key') }} *</label>
<input type="text" name="steam_key" class="form-control" required>
</div>
<div class="col-md-4">

View File

@ -3,14 +3,18 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ _('Steam Manager') }}</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ _('Game Key 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>
<a class="navbar-brand d-flex align-items-center gap-2" href="/">
<img src="{{ url_for('static', filename='logo_small.png') }}" alt="Logo" width="150" height="116" style="object-fit:contain; border-radius:8px;">
<span>Game Key Manager</span>
</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"
@ -26,28 +30,22 @@
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>
<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('export_games') }}" class="btn btn-outline-secondary">⬇️ {{ _('Export') }}</a>
<a href="{{ url_for('import_games') }}" class="btn btn-outline-secondary">⬆️ {{ _('Import') }}</a>
<a href="{{ url_for('logout') }}" class="btn btn-danger ms-3">{{ _('Logout') }}</a>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Passwort') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
</li>
{% endif %}
</div>
</div>
@ -68,16 +66,16 @@
<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;
const toggle = document.getElementById('darkModeSwitch')
const html = document.documentElement
toggle.addEventListener('change', function() {
const theme = this.checked ? 'dark' : 'light';
const theme = this.checked ? 'dark' : 'light'
fetch('/set-theme/' + theme)
.then(() => {
html.setAttribute('data-bs-theme', theme);
});
});
});
.then(() => html.setAttribute('data-bs-theme', theme))
})
})
</script>
{% include "footer.html" %}
</body>
</html>

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block content %}
<div class="card p-4 shadow-sm">
<h2 class="mb-4">{{ _('Change Password') }}</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">{{ _('Current Password') }}</label>
<input type="password" name="current_password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">{{ _('New Password') }}</label>
<input type="password" name="new_password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">{{ _('Confirm New Password') }}</label>
<input type="password" name="confirm_password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">{{ _('Change Password') }}</button>
</form>
</div>
{% endblock %}

View File

@ -3,19 +3,20 @@
<div class="card p-4 shadow-sm">
<h2 class="mb-4">{{ _('Edit Game') }}</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<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>
<label class="form-label">{{ _('Game 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>
<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>
<div class="col-md-4">
<label class="form-label">{{ _('Status') }} *</label>
<select name="status" class="form-select" required>
@ -40,6 +41,21 @@
<label class="form-label">{{ _('Notes') }}</label>
<textarea name="notes" class="form-control" rows="3">{{ game.notes }}</textarea>
</div>
<div class="col-12">
{% if redeem_url and active_redeem %}
<div class="mb-3">
<label class="form-label">{{ _('Active Redeem Link') }}</label>
<input type="text"
class="form-control"
value="{{ redeem_url }}"
readonly
onclick="this.select()">
<small class="text-muted">
{{ _('Expires at') }}: {{ active_redeem.expires.strftime('%d.%m.%Y %H:%M') }}
</small>
</div>
{% endif %}
</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>

View File

@ -0,0 +1,19 @@
<footer class="mt-5 py-4 bg-body-tertiary border-top">
<div class="container text-center small text-muted">
<div class="mb-2">
<strong>Game Key Manager</strong> &mdash; is done by nocci
</div>
<div class="mb-2">
<a href="https://git.nocci.it/nocci/GiftGamesDB" target="_blank" rel="noopener">
<img src="{{ url_for('static', filename='forgejo.svg') }}" alt="forgejo" width="20" style="vertical-align:middle;margin-right:4px;">
find the source code on my Forgejo
</a>
</div>
<div>
<span>feel free to donate - if you can affort it:</span>
<a href="https://www.paypal.me/badbramstedt" target="_blank" rel="noopener">PayPal</a> &middot;
<a href="https://ko-fi.com/nocci" target="_blank" rel="noopener">Ko-fi</a> &middot;
<a href="https://liberapay.com/nocci" target="_blank" rel="noopener">Liberapay</a>
</div>
</div>
</footer>

View File

@ -3,6 +3,7 @@
<div class="card p-4 shadow-sm">
<h2 class="mb-4">{{ _('Import Games') }}</h2>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">{{ _('CSV-Datei auswählen') }}</label>
<input type="file" name="file" class="form-control" accept=".csv" required>

View File

@ -2,9 +2,12 @@
{% 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>
<a href="{{ url_for('export_games') }}" class="btn btn-outline-secondary">⬇️ {{ _('Export CSV') }}</a>
<a href="{{ url_for('export_pdf') }}" class="btn btn-outline-secondary">⬇️ Export PDF (for sharing)</a>
<a href="{{ url_for('import_games') }}" class="btn btn-outline-secondary">⬆️ {{ _('Import CSV') }}</a>
<a href="{{ url_for('add_game') }}" class="btn btn-primary">+ {{ _('Add New Game') }}</a>
</div>
</div>
{% if games %}
@ -54,8 +57,16 @@
{% endif %}
</td>
<td class="text-nowrap">
{% if game.status == 'verschenkt' %}
<button class="btn btn-sm btn-success generate-redeem"
data-game-id="{{ game.id }}"
title="{{ _('Generate redeem link') }}">
🔗
</button>
{% endif %}
<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">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ _('Really delete?') }}')">🗑️</button>
</form>
</td>
@ -64,6 +75,33 @@
</tbody>
</table>
</div>
<script>
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
document.querySelectorAll('.generate-redeem').forEach(btn => {
btn.addEventListener('click', async function() {
const gameId = this.dataset.gameId;
try {
const response = await fetch('/generate_redeem/' + gameId, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
});
if (!response.ok) throw new Error('Network error');
const data = await response.json();
if(data.url) {
await navigator.clipboard.writeText(data.url);
alert('{{ _("Redeem link copied to clipboard!") }}');
}
} catch (error) {
console.error('Error:', error);
alert('{{ _("Error generating link") }}');
}
});
});
</script>
{% else %}
<div class="alert alert-info">{{ _('No games yet') }}</div>
{% endif %}

View File

@ -3,9 +3,11 @@
<div class="row justify-content-center mt-5">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-body">
<div class="card-body text-center">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" width="311" height="240" class="mb-4" style="object-fit:contain;">
<h2 class="card-title mb-4">{{ _('Login') }}</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">{{ _('Username') }}</label>
<input type="text" name="username" class="form-control" required>
@ -24,3 +26,4 @@
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,91 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card shadow-lg">
<div class="row g-0">
{% if game.steam_appid %}
<div class="col-md-4">
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
class="img-fluid rounded-start" alt="Game Cover">
</div>
{% endif %}
<div class="col-md-8">
<div class="card-body">
<h1 class="card-title mb-4">{{ game.name }}</h1>
<div class="alert alert-success">
<h4>{{ _('Your Key:') }}</h4>
<code class="fs-3">{{ game.steam_key }}</code>
</div>
<a href="{{ platform_link }}{{ game.steam_key }}"
class="btn btn-primary btn-lg mb-3"
target="_blank">
{{ _('Redeem now on') }} {% if game.steam_appid %}Steam{% else %}GOG{% endif %}
</a>
<div class="mt-4 text-muted">
<small>
{{ _('This page will expire in') }}
<span id="expiry-countdown" class="fw-bold"></span>
</small>
<div class="progress mt-2" style="height: 8px;">
<div id="expiry-bar"
class="progress-bar bg-danger"
role="progressbar"
style="width: 100%">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const totalDuration = {{ redeem_token.total_hours * 3600 * 1000 }}; // Gesamtdauer in Millisekunden
const expires = {{ (redeem_token.expires.timestamp() * 1000) | int }};
const countdownEl = document.getElementById('expiry-countdown');
const progressBar = document.getElementById('expiry-bar');
function formatTime(unit) {
return unit < 10 ? `0${unit}` : unit;
}
function updateProgressBar(percentage) {
// Alle Farbklassen entfernen
progressBar.classList.remove('bg-success', 'bg-warning', 'bg-danger');
if (percentage > 75) {
progressBar.classList.add('bg-success');
} else if (percentage > 25) {
progressBar.classList.add('bg-warning');
} else {
progressBar.classList.add('bg-danger');
}
}
function updateCountdown() {
const now = Date.now();
const remaining = expires - now;
const percent = (remaining / totalDuration) * 100;
if (remaining < 0) {
countdownEl.innerHTML = "EXPIRED";
progressBar.style.width = "0%";
clearInterval(timer);
setTimeout(() => window.location.reload(), 5000);
return;
}
const hours = Math.floor(remaining / (1000 * 60 * 60));
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((remaining % (1000 * 60)) / 1000);
countdownEl.innerHTML = `${formatTime(hours)}h ${formatTime(minutes)}m ${formatTime(seconds)}s`;
progressBar.style.width = `${percent}%`;
updateProgressBar(percent);
}
// Initialisierung
updateCountdown();
const timer = setInterval(updateCountdown, 1000);
</script>
{% endblock %}

View File

@ -6,6 +6,7 @@
<div class="card-body">
<h2 class="card-title mb-4">{{ _('Register') }}</h2>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label class="form-label">{{ _('Username') }}</label>
<input type="text" name="username" class="form-control" required>

View File

@ -1,216 +0,0 @@
# German translations for PROJECT.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-04-22 11:22+0000\n"
"PO-Revision-Date: 2025-04-22 11:22+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
"Language-Team: de <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: app.py:94
msgid "Invalid credentials"
msgstr "Ooops. Falsche Benutzerdaten!"
#: app.py:103
msgid "Username already exists"
msgstr "Benutzer existiert bereits"
#: app.py:148
msgid "Game added successfully!"
msgstr "Spiel erfolgreich hinzugefügt!"
#: app.py:152 app.py:181
msgid "Error: "
msgstr "Ui. Ein Fehler: "
#: app.py:160 app.py:191
msgid "Not allowed!"
msgstr "Das ist nicht erlaubt!"
#: app.py:177
msgid "Changes saved!"
msgstr "Änderungen gespeichert!"
#: app.py:195
msgid "Game deleted!"
msgstr "Spiel gelöscht!"
#: app.py:198
msgid "Error deleting: "
msgstr "Fehler beim Löschen: "
#: app.py:248
msgid "Import erfolgreich!"
msgstr "Import erfolgreich!"
#: app.py:250
msgid "Bitte eine gültige CSV-Datei hochladen."
msgstr "Bitte eine gültige CSV-Datei hochladen."
#: templates/add_game.html:4 templates/index.html:6
msgid "Add New Game"
msgstr "Spiel hinzufügen"
#: templates/add_game.html:8 templates/edit_game.html:8 templates/index.html:16
msgid "Name"
msgstr "Name"
#: templates/add_game.html:12 templates/edit_game.html:12
msgid "Steam Key"
msgstr "Game Key"
#: templates/add_game.html:16 templates/edit_game.html:20
#: templates/index.html:18
msgid "Status"
msgstr "Status"
#: templates/add_game.html:18 templates/edit_game.html:22
#: templates/index.html:38
msgid "Not redeemed"
msgstr "Nicht eingelöst"
#: templates/add_game.html:19 templates/edit_game.html:23
#: templates/index.html:40
msgid "Gifted"
msgstr "Verschenkt"
#: templates/add_game.html:20 templates/edit_game.html:24
#: templates/index.html:42
msgid "Redeemed"
msgstr "Eingelöst"
#: templates/add_game.html:24 templates/edit_game.html:28
#: templates/index.html:20
msgid "Redeem by"
msgstr "Einzulösen vor"
#: templates/add_game.html:28 templates/edit_game.html:32
msgid "Recipient"
msgstr "Empfänger*in"
#: templates/add_game.html:32 templates/edit_game.html:36
msgid "Shop URL"
msgstr "Shop URL"
#: templates/add_game.html:36 templates/edit_game.html:40
msgid "Notes"
msgstr "Notizen"
#: templates/add_game.html:40 templates/edit_game.html:44
msgid "Save"
msgstr "Gespeichert"
#: templates/add_game.html:41 templates/edit_game.html:45
msgid "Cancel"
msgstr "Abbrechen"
#: templates/base.html:6 templates/base.html:13
msgid "Steam Manager"
msgstr "Steam Spiele Manager"
#: templates/base.html:19
msgid "Search"
msgstr "Suche"
#: templates/base.html:27
msgid "Dark Mode"
msgstr "Dark Mode"
#: templates/base.html:48
msgid "Export"
msgstr "Export"
#: templates/base.html:49
msgid "Import"
msgstr "Import"
#: templates/base.html:50
msgid "Logout"
msgstr "Logout"
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr "Spiel editieren"
#: templates/edit_game.html:16
msgid "Steam AppID (optional)"
msgstr ""
#: templates/import.html:4
msgid "Import Games"
msgstr "Importiere Spiele"
#: templates/import.html:7
msgid "CSV-Datei auswählen"
msgstr ""
#: templates/import.html:10
msgid "Importieren"
msgstr ""
#: templates/import.html:11
msgid "Abbrechen"
msgstr ""
#: templates/index.html:4
msgid "My Games"
msgstr "Meine Spiele"
#: templates/index.html:15
msgid "Cover"
msgstr "Cover"
#: templates/index.html:17
msgid "Key"
msgstr "Key"
#: templates/index.html:19
msgid "Created"
msgstr "Erstellt"
#: templates/index.html:21 templates/index.html:53
msgid "Shop"
msgstr "ShopLink"
#: templates/index.html:22
msgid "Actions"
msgstr "Aktionen"
#: templates/index.html:59
msgid "Really delete?"
msgstr "Wirklich löschen?"
#: templates/index.html:68
msgid "No games yet"
msgstr "Der Kornspeicher ist leer, Sire!"
#: templates/login.html:7 templates/login.html:17
msgid "Login"
msgstr "Anmelden"
#: templates/login.html:10 templates/register.html:10
msgid "Username"
msgstr "Benutzername"
#: templates/login.html:14 templates/register.html:14
msgid "Password"
msgstr "Passwort"
#: templates/login.html:20
msgid "No account yet? Register"
msgstr "Kein Account? Hier registrieren!"
#: templates/register.html:7 templates/register.html:17
msgid "Register"
msgstr "Registrieren"

View File

@ -1,217 +0,0 @@
# English translations for PROJECT.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-04-22 11:22+0000\n"
"PO-Revision-Date: 2025-04-22 11:22+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: app.py:94
msgid "Invalid credentials"
msgstr ""
#: app.py:103
msgid "Username already exists"
msgstr ""
#: app.py:148
msgid "Game added successfully!"
msgstr ""
#: app.py:152 app.py:181
msgid "Error: "
msgstr ""
#: app.py:160 app.py:191
msgid "Not allowed!"
msgstr ""
#: app.py:177
msgid "Changes saved!"
msgstr ""
#: app.py:195
msgid "Game deleted!"
msgstr ""
#: app.py:198
msgid "Error deleting: "
msgstr ""
#: app.py:248
msgid "Import erfolgreich!"
msgstr ""
#: app.py:250
msgid "Bitte eine gültige CSV-Datei hochladen."
msgstr ""
#: templates/add_game.html:4 templates/index.html:6
msgid "Add New Game"
msgstr ""
#: templates/add_game.html:8 templates/edit_game.html:8 templates/index.html:16
msgid "Name"
msgstr ""
#: templates/add_game.html:12 templates/edit_game.html:12
msgid "Steam Key"
msgstr ""
#: templates/add_game.html:16 templates/edit_game.html:20
#: templates/index.html:18
msgid "Status"
msgstr ""
#: templates/add_game.html:18 templates/edit_game.html:22
#: templates/index.html:38
msgid "Not redeemed"
msgstr ""
#: templates/add_game.html:19 templates/edit_game.html:23
#: templates/index.html:40
msgid "Gifted"
msgstr ""
#: templates/add_game.html:20 templates/edit_game.html:24
#: templates/index.html:42
msgid "Redeemed"
msgstr ""
#: templates/add_game.html:24 templates/edit_game.html:28
#: templates/index.html:20
msgid "Redeem by"
msgstr ""
#: templates/add_game.html:28 templates/edit_game.html:32
msgid "Recipient"
msgstr ""
#: templates/add_game.html:32 templates/edit_game.html:36
msgid "Shop URL"
msgstr ""
#: templates/add_game.html:36 templates/edit_game.html:40
msgid "Notes"
msgstr ""
#: templates/add_game.html:40 templates/edit_game.html:44
msgid "Save"
msgstr ""
#: templates/add_game.html:41 templates/edit_game.html:45
msgid "Cancel"
msgstr ""
#: templates/base.html:6 templates/base.html:13
msgid "Steam Manager"
msgstr ""
#: templates/base.html:19
msgid "Search"
msgstr ""
#: templates/base.html:27
msgid "Dark Mode"
msgstr ""
#: templates/base.html:48
msgid "Export"
msgstr ""
#: templates/base.html:49
msgid "Import"
msgstr ""
#: templates/base.html:50
msgid "Logout"
msgstr ""
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr ""
#: templates/edit_game.html:16
msgid "Steam AppID (optional)"
msgstr ""
#: templates/import.html:4
msgid "Import Games"
msgstr ""
#: templates/import.html:7
msgid "CSV-Datei auswählen"
msgstr ""
#: templates/import.html:10
msgid "Importieren"
msgstr ""
#: templates/import.html:11
msgid "Abbrechen"
msgstr ""
#: templates/index.html:4
msgid "My Games"
msgstr ""
#: templates/index.html:15
msgid "Cover"
msgstr ""
#: templates/index.html:17
msgid "Key"
msgstr ""
#: templates/index.html:19
msgid "Created"
msgstr ""
#: templates/index.html:21 templates/index.html:53
msgid "Shop"
msgstr ""
#: templates/index.html:22
msgid "Actions"
msgstr ""
#: templates/index.html:59
msgid "Really delete?"
msgstr ""
#: templates/index.html:68
msgid "No games yet"
msgstr ""
#: templates/login.html:7 templates/login.html:17
msgid "Login"
msgstr ""
#: templates/login.html:10 templates/register.html:10
msgid "Username"
msgstr ""
#: templates/login.html:14 templates/register.html:14
msgid "Password"
msgstr ""
#: templates/login.html:20
msgid "No account yet? Register"
msgstr ""
#: templates/register.html:7 templates/register.html:17
msgid "Register"
msgstr ""

View File

@ -1,216 +0,0 @@
# Translations template for PROJECT.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-04-22 11:22+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: app.py:94
msgid "Invalid credentials"
msgstr ""
#: app.py:103
msgid "Username already exists"
msgstr ""
#: app.py:148
msgid "Game added successfully!"
msgstr ""
#: app.py:152 app.py:181
msgid "Error: "
msgstr ""
#: app.py:160 app.py:191
msgid "Not allowed!"
msgstr ""
#: app.py:177
msgid "Changes saved!"
msgstr ""
#: app.py:195
msgid "Game deleted!"
msgstr ""
#: app.py:198
msgid "Error deleting: "
msgstr ""
#: app.py:248
msgid "Import erfolgreich!"
msgstr ""
#: app.py:250
msgid "Bitte eine gültige CSV-Datei hochladen."
msgstr ""
#: templates/add_game.html:4 templates/index.html:6
msgid "Add New Game"
msgstr ""
#: templates/add_game.html:8 templates/edit_game.html:8 templates/index.html:16
msgid "Name"
msgstr ""
#: templates/add_game.html:12 templates/edit_game.html:12
msgid "Steam Key"
msgstr ""
#: templates/add_game.html:16 templates/edit_game.html:20
#: templates/index.html:18
msgid "Status"
msgstr ""
#: templates/add_game.html:18 templates/edit_game.html:22
#: templates/index.html:38
msgid "Not redeemed"
msgstr ""
#: templates/add_game.html:19 templates/edit_game.html:23
#: templates/index.html:40
msgid "Gifted"
msgstr ""
#: templates/add_game.html:20 templates/edit_game.html:24
#: templates/index.html:42
msgid "Redeemed"
msgstr ""
#: templates/add_game.html:24 templates/edit_game.html:28
#: templates/index.html:20
msgid "Redeem by"
msgstr ""
#: templates/add_game.html:28 templates/edit_game.html:32
msgid "Recipient"
msgstr ""
#: templates/add_game.html:32 templates/edit_game.html:36
msgid "Shop URL"
msgstr ""
#: templates/add_game.html:36 templates/edit_game.html:40
msgid "Notes"
msgstr ""
#: templates/add_game.html:40 templates/edit_game.html:44
msgid "Save"
msgstr ""
#: templates/add_game.html:41 templates/edit_game.html:45
msgid "Cancel"
msgstr ""
#: templates/base.html:6 templates/base.html:13
msgid "Steam Manager"
msgstr ""
#: templates/base.html:19
msgid "Search"
msgstr ""
#: templates/base.html:27
msgid "Dark Mode"
msgstr ""
#: templates/base.html:48
msgid "Export"
msgstr ""
#: templates/base.html:49
msgid "Import"
msgstr ""
#: templates/base.html:50
msgid "Logout"
msgstr ""
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr ""
#: templates/edit_game.html:16
msgid "Steam AppID (optional)"
msgstr ""
#: templates/import.html:4
msgid "Import Games"
msgstr ""
#: templates/import.html:7
msgid "CSV-Datei auswählen"
msgstr ""
#: templates/import.html:10
msgid "Importieren"
msgstr ""
#: templates/import.html:11
msgid "Abbrechen"
msgstr ""
#: templates/index.html:4
msgid "My Games"
msgstr ""
#: templates/index.html:15
msgid "Cover"
msgstr ""
#: templates/index.html:17
msgid "Key"
msgstr ""
#: templates/index.html:19
msgid "Created"
msgstr ""
#: templates/index.html:21 templates/index.html:53
msgid "Shop"
msgstr ""
#: templates/index.html:22
msgid "Actions"
msgstr ""
#: templates/index.html:59
msgid "Really delete?"
msgstr ""
#: templates/index.html:68
msgid "No games yet"
msgstr ""
#: templates/login.html:7 templates/login.html:17
msgid "Login"
msgstr ""
#: templates/login.html:10 templates/register.html:10
msgid "Username"
msgstr ""
#: templates/login.html:14 templates/register.html:14
msgid "Password"
msgstr ""
#: templates/login.html:20
msgid "No account yet? Register"
msgstr ""
#: templates/register.html:7 templates/register.html:17
msgid "Register"
msgstr ""

View File

@ -3,20 +3,26 @@ set -e
cd "$(dirname "$0")/steam-gift-manager"
# 1. Extrahiere alle Texte
declare -A locales=(
["de"]="de"
["en"]="en"
)
# POT-Datei erstellen
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
# Für jede Sprache prüfen und ggf. initialisieren
for lang in "${!locales[@]}"; do
if [ ! -f "translations/${locales[$lang]}/LC_MESSAGES/messages.po" ]; then
docker-compose exec steam-manager pybabel init \
-i translations/messages.pot \
-d translations \
-l "${locales[$lang]}"
fi
done
# 3. Aktualisiere Übersetzungen
# Übersetzungen aktualisieren und kompilieren
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!"
echo "✅ Übersetzungen aktualisiert!"

View File

@ -0,0 +1,287 @@
# German translations for PROJECT.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-04-26 11:13+0000\n"
"PO-Revision-Date: 2025-04-26 11:13+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
"Language-Team: de <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: app.py:187
msgid "Invalid credentials"
msgstr ""
#: app.py:193
msgid "Registrierungen sind deaktiviert"
msgstr ""
#: app.py:201
msgid "Username already exists"
msgstr ""
#: app.py:227
msgid "Aktuelles Passwort ist falsch"
msgstr ""
#: app.py:231
msgid "Neue Passwörter stimmen nicht überein"
msgstr ""
#: app.py:236
msgid "Passwort erfolgreich geändert"
msgstr ""
#: app.py:266
msgid "Game added successfully!"
msgstr ""
#: app.py:271
msgid "Steam Key already exists!"
msgstr ""
#: app.py:274 app.py:318
msgid "Error: "
msgstr ""
#: app.py:313
msgid "Changes saved!"
msgstr ""
#: app.py:401
msgid "Game List (without Keys)"
msgstr ""
#: app.py:494
#, python-format
msgid "%(new)d neue Spiele importiert, %(dup)d Duplikate übersprungen"
msgstr ""
#: app.py:498
#, python-format
msgid "Importfehler: %(error)s"
msgstr ""
#: app.py:502
msgid "Bitte eine gültige CSV-Datei hochladen."
msgstr ""
#: templates/add_game.html:4 templates/index.html:9
msgid "Add New Game"
msgstr ""
#: templates/add_game.html:9 templates/edit_game.html:9 templates/index.html:19
msgid "Name"
msgstr ""
#: templates/add_game.html:13 templates/edit_game.html:13
msgid "Game Key"
msgstr ""
#: templates/add_game.html:17 templates/edit_game.html:21
#: templates/index.html:21
msgid "Status"
msgstr ""
#: templates/add_game.html:19 templates/edit_game.html:23
#: templates/index.html:41
msgid "Not redeemed"
msgstr ""
#: templates/add_game.html:20 templates/edit_game.html:24
#: templates/index.html:43
msgid "Gifted"
msgstr ""
#: templates/add_game.html:21 templates/edit_game.html:25
#: templates/index.html:45
msgid "Redeemed"
msgstr ""
#: templates/add_game.html:25 templates/edit_game.html:29
#: templates/index.html:23
msgid "Redeem by"
msgstr ""
#: templates/add_game.html:29 templates/edit_game.html:33
msgid "Recipient"
msgstr ""
#: templates/add_game.html:33 templates/edit_game.html:37
msgid "Shop URL"
msgstr ""
#: templates/add_game.html:37 templates/edit_game.html:41
msgid "Notes"
msgstr ""
#: templates/add_game.html:41 templates/edit_game.html:60
msgid "Save"
msgstr ""
#: templates/add_game.html:42 templates/edit_game.html:61
msgid "Cancel"
msgstr ""
#: templates/base.html:7
msgid "Game Key Manager"
msgstr ""
#: templates/base.html:23
msgid "Search"
msgstr ""
#: templates/base.html:31
msgid "Dark Mode"
msgstr ""
#: templates/base.html:44
msgid "Passwort"
msgstr ""
#: templates/base.html:47
msgid "Logout"
msgstr ""
#: templates/change_password.html:4 templates/change_password.html:19
msgid "Change Password"
msgstr ""
#: templates/change_password.html:8
msgid "Current Password"
msgstr ""
#: templates/change_password.html:12
msgid "New Password"
msgstr ""
#: templates/change_password.html:16
msgid "Confirm New Password"
msgstr ""
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr ""
#: templates/edit_game.html:17
msgid "Steam AppID (optional)"
msgstr ""
#: templates/edit_game.html:47
msgid "Active Redeem Link"
msgstr ""
#: templates/edit_game.html:54
msgid "Expires at"
msgstr ""
#: templates/import.html:4
msgid "Import Games"
msgstr ""
#: templates/import.html:8
msgid "CSV-Datei auswählen"
msgstr ""
#: templates/import.html:11
msgid "Importieren"
msgstr ""
#: templates/import.html:12
msgid "Abbrechen"
msgstr ""
#: templates/index.html:4
msgid "My Games"
msgstr ""
#: templates/index.html:6
msgid "Export CSV"
msgstr ""
#: templates/index.html:8
msgid "Import CSV"
msgstr ""
#: templates/index.html:18
msgid "Cover"
msgstr ""
#: templates/index.html:20
msgid "Key"
msgstr ""
#: templates/index.html:22
msgid "Created"
msgstr ""
#: templates/index.html:24 templates/index.html:56
msgid "Shop"
msgstr ""
#: templates/index.html:25
msgid "Actions"
msgstr ""
#: templates/index.html:63
msgid "Generate redeem link"
msgstr ""
#: templates/index.html:70
msgid "Really delete?"
msgstr ""
#: templates/index.html:96
msgid "Redeem link copied to clipboard!"
msgstr ""
#: templates/index.html:100
msgid "Error generating link"
msgstr ""
#: templates/index.html:106
msgid "No games yet"
msgstr ""
#: templates/login.html:8 templates/login.html:19
msgid "Login"
msgstr ""
#: templates/login.html:12 templates/register.html:11
msgid "Username"
msgstr ""
#: templates/login.html:16 templates/register.html:15
msgid "Password"
msgstr ""
#: templates/login.html:22
msgid "No account yet? Register"
msgstr ""
#: templates/redeem.html:16
msgid "Your Key:"
msgstr ""
#: templates/redeem.html:22
msgid "Redeem now on"
msgstr ""
#: templates/redeem.html:26
msgid "This page will expire in"
msgstr ""
#: templates/register.html:7 templates/register.html:18
msgid "Register"
msgstr ""

View File

@ -0,0 +1,287 @@
# English translations for PROJECT.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-04-26 11:13+0000\n"
"PO-Revision-Date: 2025-04-26 11:13+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: app.py:187
msgid "Invalid credentials"
msgstr ""
#: app.py:193
msgid "Registrierungen sind deaktiviert"
msgstr ""
#: app.py:201
msgid "Username already exists"
msgstr ""
#: app.py:227
msgid "Aktuelles Passwort ist falsch"
msgstr ""
#: app.py:231
msgid "Neue Passwörter stimmen nicht überein"
msgstr ""
#: app.py:236
msgid "Passwort erfolgreich geändert"
msgstr ""
#: app.py:266
msgid "Game added successfully!"
msgstr ""
#: app.py:271
msgid "Steam Key already exists!"
msgstr ""
#: app.py:274 app.py:318
msgid "Error: "
msgstr ""
#: app.py:313
msgid "Changes saved!"
msgstr ""
#: app.py:401
msgid "Game List (without Keys)"
msgstr ""
#: app.py:494
#, python-format
msgid "%(new)d neue Spiele importiert, %(dup)d Duplikate übersprungen"
msgstr ""
#: app.py:498
#, python-format
msgid "Importfehler: %(error)s"
msgstr ""
#: app.py:502
msgid "Bitte eine gültige CSV-Datei hochladen."
msgstr ""
#: templates/add_game.html:4 templates/index.html:9
msgid "Add New Game"
msgstr ""
#: templates/add_game.html:9 templates/edit_game.html:9 templates/index.html:19
msgid "Name"
msgstr ""
#: templates/add_game.html:13 templates/edit_game.html:13
msgid "Game Key"
msgstr ""
#: templates/add_game.html:17 templates/edit_game.html:21
#: templates/index.html:21
msgid "Status"
msgstr ""
#: templates/add_game.html:19 templates/edit_game.html:23
#: templates/index.html:41
msgid "Not redeemed"
msgstr ""
#: templates/add_game.html:20 templates/edit_game.html:24
#: templates/index.html:43
msgid "Gifted"
msgstr ""
#: templates/add_game.html:21 templates/edit_game.html:25
#: templates/index.html:45
msgid "Redeemed"
msgstr ""
#: templates/add_game.html:25 templates/edit_game.html:29
#: templates/index.html:23
msgid "Redeem by"
msgstr ""
#: templates/add_game.html:29 templates/edit_game.html:33
msgid "Recipient"
msgstr ""
#: templates/add_game.html:33 templates/edit_game.html:37
msgid "Shop URL"
msgstr ""
#: templates/add_game.html:37 templates/edit_game.html:41
msgid "Notes"
msgstr ""
#: templates/add_game.html:41 templates/edit_game.html:60
msgid "Save"
msgstr ""
#: templates/add_game.html:42 templates/edit_game.html:61
msgid "Cancel"
msgstr ""
#: templates/base.html:7
msgid "Game Key Manager"
msgstr ""
#: templates/base.html:23
msgid "Search"
msgstr ""
#: templates/base.html:31
msgid "Dark Mode"
msgstr ""
#: templates/base.html:44
msgid "Passwort"
msgstr ""
#: templates/base.html:47
msgid "Logout"
msgstr ""
#: templates/change_password.html:4 templates/change_password.html:19
msgid "Change Password"
msgstr ""
#: templates/change_password.html:8
msgid "Current Password"
msgstr ""
#: templates/change_password.html:12
msgid "New Password"
msgstr ""
#: templates/change_password.html:16
msgid "Confirm New Password"
msgstr ""
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr ""
#: templates/edit_game.html:17
msgid "Steam AppID (optional)"
msgstr ""
#: templates/edit_game.html:47
msgid "Active Redeem Link"
msgstr ""
#: templates/edit_game.html:54
msgid "Expires at"
msgstr ""
#: templates/import.html:4
msgid "Import Games"
msgstr ""
#: templates/import.html:8
msgid "CSV-Datei auswählen"
msgstr ""
#: templates/import.html:11
msgid "Importieren"
msgstr ""
#: templates/import.html:12
msgid "Abbrechen"
msgstr ""
#: templates/index.html:4
msgid "My Games"
msgstr ""
#: templates/index.html:6
msgid "Export CSV"
msgstr ""
#: templates/index.html:8
msgid "Import CSV"
msgstr ""
#: templates/index.html:18
msgid "Cover"
msgstr ""
#: templates/index.html:20
msgid "Key"
msgstr ""
#: templates/index.html:22
msgid "Created"
msgstr ""
#: templates/index.html:24 templates/index.html:56
msgid "Shop"
msgstr ""
#: templates/index.html:25
msgid "Actions"
msgstr ""
#: templates/index.html:63
msgid "Generate redeem link"
msgstr ""
#: templates/index.html:70
msgid "Really delete?"
msgstr ""
#: templates/index.html:96
msgid "Redeem link copied to clipboard!"
msgstr ""
#: templates/index.html:100
msgid "Error generating link"
msgstr ""
#: templates/index.html:106
msgid "No games yet"
msgstr ""
#: templates/login.html:8 templates/login.html:19
msgid "Login"
msgstr ""
#: templates/login.html:12 templates/register.html:11
msgid "Username"
msgstr ""
#: templates/login.html:16 templates/register.html:15
msgid "Password"
msgstr ""
#: templates/login.html:22
msgid "No account yet? Register"
msgstr ""
#: templates/redeem.html:16
msgid "Your Key:"
msgstr ""
#: templates/redeem.html:22
msgid "Redeem now on"
msgstr ""
#: templates/redeem.html:26
msgid "This page will expire in"
msgstr ""
#: templates/register.html:7 templates/register.html:18
msgid "Register"
msgstr ""

286
translations/messages.pot Normal file
View File

@ -0,0 +1,286 @@
# Translations template for PROJECT.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-04-26 11:13+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: app.py:187
msgid "Invalid credentials"
msgstr ""
#: app.py:193
msgid "Registrierungen sind deaktiviert"
msgstr ""
#: app.py:201
msgid "Username already exists"
msgstr ""
#: app.py:227
msgid "Aktuelles Passwort ist falsch"
msgstr ""
#: app.py:231
msgid "Neue Passwörter stimmen nicht überein"
msgstr ""
#: app.py:236
msgid "Passwort erfolgreich geändert"
msgstr ""
#: app.py:266
msgid "Game added successfully!"
msgstr ""
#: app.py:271
msgid "Steam Key already exists!"
msgstr ""
#: app.py:274 app.py:318
msgid "Error: "
msgstr ""
#: app.py:313
msgid "Changes saved!"
msgstr ""
#: app.py:401
msgid "Game List (without Keys)"
msgstr ""
#: app.py:494
#, python-format
msgid "%(new)d neue Spiele importiert, %(dup)d Duplikate übersprungen"
msgstr ""
#: app.py:498
#, python-format
msgid "Importfehler: %(error)s"
msgstr ""
#: app.py:502
msgid "Bitte eine gültige CSV-Datei hochladen."
msgstr ""
#: templates/add_game.html:4 templates/index.html:9
msgid "Add New Game"
msgstr ""
#: templates/add_game.html:9 templates/edit_game.html:9 templates/index.html:19
msgid "Name"
msgstr ""
#: templates/add_game.html:13 templates/edit_game.html:13
msgid "Game Key"
msgstr ""
#: templates/add_game.html:17 templates/edit_game.html:21
#: templates/index.html:21
msgid "Status"
msgstr ""
#: templates/add_game.html:19 templates/edit_game.html:23
#: templates/index.html:41
msgid "Not redeemed"
msgstr ""
#: templates/add_game.html:20 templates/edit_game.html:24
#: templates/index.html:43
msgid "Gifted"
msgstr ""
#: templates/add_game.html:21 templates/edit_game.html:25
#: templates/index.html:45
msgid "Redeemed"
msgstr ""
#: templates/add_game.html:25 templates/edit_game.html:29
#: templates/index.html:23
msgid "Redeem by"
msgstr ""
#: templates/add_game.html:29 templates/edit_game.html:33
msgid "Recipient"
msgstr ""
#: templates/add_game.html:33 templates/edit_game.html:37
msgid "Shop URL"
msgstr ""
#: templates/add_game.html:37 templates/edit_game.html:41
msgid "Notes"
msgstr ""
#: templates/add_game.html:41 templates/edit_game.html:60
msgid "Save"
msgstr ""
#: templates/add_game.html:42 templates/edit_game.html:61
msgid "Cancel"
msgstr ""
#: templates/base.html:7
msgid "Game Key Manager"
msgstr ""
#: templates/base.html:23
msgid "Search"
msgstr ""
#: templates/base.html:31
msgid "Dark Mode"
msgstr ""
#: templates/base.html:44
msgid "Passwort"
msgstr ""
#: templates/base.html:47
msgid "Logout"
msgstr ""
#: templates/change_password.html:4 templates/change_password.html:19
msgid "Change Password"
msgstr ""
#: templates/change_password.html:8
msgid "Current Password"
msgstr ""
#: templates/change_password.html:12
msgid "New Password"
msgstr ""
#: templates/change_password.html:16
msgid "Confirm New Password"
msgstr ""
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr ""
#: templates/edit_game.html:17
msgid "Steam AppID (optional)"
msgstr ""
#: templates/edit_game.html:47
msgid "Active Redeem Link"
msgstr ""
#: templates/edit_game.html:54
msgid "Expires at"
msgstr ""
#: templates/import.html:4
msgid "Import Games"
msgstr ""
#: templates/import.html:8
msgid "CSV-Datei auswählen"
msgstr ""
#: templates/import.html:11
msgid "Importieren"
msgstr ""
#: templates/import.html:12
msgid "Abbrechen"
msgstr ""
#: templates/index.html:4
msgid "My Games"
msgstr ""
#: templates/index.html:6
msgid "Export CSV"
msgstr ""
#: templates/index.html:8
msgid "Import CSV"
msgstr ""
#: templates/index.html:18
msgid "Cover"
msgstr ""
#: templates/index.html:20
msgid "Key"
msgstr ""
#: templates/index.html:22
msgid "Created"
msgstr ""
#: templates/index.html:24 templates/index.html:56
msgid "Shop"
msgstr ""
#: templates/index.html:25
msgid "Actions"
msgstr ""
#: templates/index.html:63
msgid "Generate redeem link"
msgstr ""
#: templates/index.html:70
msgid "Really delete?"
msgstr ""
#: templates/index.html:96
msgid "Redeem link copied to clipboard!"
msgstr ""
#: templates/index.html:100
msgid "Error generating link"
msgstr ""
#: templates/index.html:106
msgid "No games yet"
msgstr ""
#: templates/login.html:8 templates/login.html:19
msgid "Login"
msgstr ""
#: templates/login.html:12 templates/register.html:11
msgid "Username"
msgstr ""
#: templates/login.html:16 templates/register.html:15
msgid "Password"
msgstr ""
#: templates/login.html:22
msgid "No account yet? Register"
msgstr ""
#: templates/redeem.html:16
msgid "Your Key:"
msgstr ""
#: templates/redeem.html:22
msgid "Redeem now on"
msgstr ""
#: templates/redeem.html:26
msgid "This page will expire in"
msgstr ""
#: templates/register.html:7 templates/register.html:18
msgid "Register"
msgstr ""

22
upgrade.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -e
# Setze das Arbeitsverzeichnis auf das Projektverzeichnis
cd "$(dirname "$0")/steam-gift-manager"
# Setze FLASK_APP, falls nötig
export FLASK_APP=app.py
# Initialisiere migrations, falls noch nicht vorhanden
if [ ! -d migrations ]; then
echo "Starting Flask-Migrate..."
docker-compose exec steam-manager flask db init
fi
# Erzeuge Migration (nur wenn sich Modelle geändert haben)
docker-compose exec steam-manager flask db migrate -m "Automatic Migration"
# Wende Migration an
docker-compose exec steam-manager flask db upgrade
echo "✅ Database-Migration abgeschlossen!"