Compare commits

...

31 Commits
0.1.9 ... main

Author SHA1 Message Date
nocci b1aee9e5b8 Urgent Fix for Translations 2025-04-29 18:02:22 +02:00
nocci fb6a2799ce Merge branch 'dev' 2025-04-29 15:21:10 +02:00
nocci d3eb37ebff more translations fixed 2025-04-29 15:19:59 +02:00
nocci c886d5f28e translations of setup.sh 2025-04-29 14:46:55 +02:00
nocci 2ea3a8059c typo in Dockerfile 2025-04-29 14:32:45 +02:00
nocci 277bd5a283 switch webserver to gunicorn 2025-04-29 14:27:55 +02:00
nocci 653d04e76a docker compose missing in setup.sh 2025-04-29 14:13:27 +02:00
nocci 894eef8657 Merge branch 'main' of https://git.nocci.it/nocci/GiftGamesDB 2025-04-29 14:00:43 +02:00
nocci 335dbdbf63 new readme bc of codeberg 2025-04-29 14:00:40 +02:00
nocci 96abe3c2b0 Merge remote-tracking branch 'origin/main' into dev 2025-04-29 13:58:27 +02:00
nocci d4e85ea44f update readme due release on codeberg 2025-04-29 12:51:23 +02:00
nocci 64d445791e uppsi / wrong folder 2025-04-28 16:21:13 +00:00
nocci 7bf20396e6 Merge branch 'main' of https://git.nocci.it/nocci/GiftGamesDB 2025-04-28 12:33:36 +02:00
nocci 380376c16b Merge branch 'dev' 2025-04-28 12:31:52 +02:00
nocci 3c5b554de1 upgrade 2025-04-28 12:19:56 +02:00
nocci f2bd4cdbb4 footer fix 2025-04-26 15:36:13 +02:00
nocci f5dd3483f7 footer fix 2025-04-26 15:35:45 +02:00
nocci 6182022222 login picture size 2025-04-26 15:18:37 +02:00
nocci db2a6556eb Version 1.0 2025-04-26 15:11:27 +02:00
nocci 61c2a1dcd7 .env gelöscht 2025-04-26 12:37:52 +00:00
nocci eaa508a8df Version 1.0 2025-04-26 14:32:07 +02:00
nocci d9bca6f24c upload image 2025-04-25 12:07:52 +00:00
nocci bd0cc68ab2 upload image 2025-04-25 12:07:43 +00:00
nocci e61f3ab4e6 upload image 2025-04-25 12:07:32 +00:00
nocci 4bebbb27e4 ready for version 0.2 2025-04-22 13:45:13 +02:00
nocci 3a79036ec4 german locale update 2025-04-22 13:34:00 +02:00
nocci e52f24554f typo 2025-04-22 13:21:50 +02:00
nocci 82c78c13cd integrate import/export funcion 2025-04-22 13:20:15 +02:00
nocci c0f462d6b6 typos 2025-04-22 11:10:29 +02:00
nocci 31c1fd1907 typos 2025-04-22 11:08:43 +02:00
nocci 491ebb2423 typos 2025-04-22 11:06:32 +02:00
33 changed files with 2924 additions and 883 deletions

34
.env Normal file
View File

@ -0,0 +1,34 @@
# Flask-Configuration - Key are generated through setup.sh
SECRET_KEY=""
REDEEM_SECRET=""
WTF_CSRF_SECRET_KEY=""
# 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

133
README.md
View File

@ -1,50 +1,78 @@
# 🗝️ Steam Key Management System 🔑 # 🔑 Game Key Manager 🔑
![Screenshot](GameManager.png) ## 👋 Welcome! 👋
## Welcome! 👋
This project helps you keep track of your collected game keys. 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! 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 ✨ ## ✨ Features ✨
- **Key Management:** - **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:** - **Status Tracking:**
Mark keys as "Redeemed", "Gifted" or "Available" always know your status. Mark keys as "Redeemed", "Gifted" or "Available" always know your status.
- **Shop URL & Steam Cover:** - **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. 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:** - **Multi-user:**
Each user manages their own keys. 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. Find games quickly with the search function.
- **Responsive UI:** - **Responsive UI:**
Works on desktop and mobile, with Dark Mode toggle. Works on desktop and mobile, with Dark Mode toggle.
- **Multi-language:** - **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!** - **No key data leaves your server!**
- **(Planned):** - **(Planned):**
- Import/Export (CSV, JSON) - ~~Import/Export (CSV)~~
- Redeem site with unique sharing link - ~~Redeem site with unique sharing link~~
--- ---
## 🚀 Get Started! 🚀 ## 🚀 Get Started! 🚀
### 1. **Clone the Repository** ## 1. **Clone the Repository (Option 1 or Option 2)**
```
git clone https://git.nocci.it/nocci/GiftGamesDB ### Option 1: Clone the main repository
```bash
git clone https://codeberg.org/nocci/GameKeyManager
cd steam-gift-manager cd steam-gift-manager
``` ```
### Option 2: Clone from alternative repository (if option 1 fails)
```bash
git clone https://git.nocci.it/nocci/GameKeyManager
```
### 2. **Setup Docker** ### 2. **Setup Docker**
Make sure you have [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) installed. 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** ### 3. **Initial Setup**
```
```bash
chmod +x setup.sh chmod +x setup.sh
./setup.sh ./setup.sh
``` ```
@ -52,58 +80,97 @@ chmod +x setup.sh
This script prepares all directories, configuration, and translation files. This script prepares all directories, configuration, and translation files.
### 4. **Build and Start the App** ### 4. **Build and Start the App**
```
```bash
cd steam-gift-manager/ cd steam-gift-manager/
docker-compose build --no-cache docker-compose build --no-cache
docker-compose up -d docker-compose up -d
``` ```
### 5. **Initialize and Edit Translations (Optional)** ### 5. **Edit your .env file to your liking**
```
./translate.sh It's in your root folder of the installation!
```
Edit the .po files in steam-translations/de/LC_MESSAGES/messages.po and en/LC_MESSAGES/messages.po ```xml
``` # Security
./translate.sh SESSION_COOKIE_SECURE="True" (only works if you run this app via HTTPS)
cd steam-gift-manager/ CSRF_ENABLED="True"
docker-compose restart steam-manager
``` ```
### 6. **Open the App** **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 translations/de_DE/LC_MESSAGES/messages.po and en_US/LC_MESSAGES/messages.po
```bash
./translate.sh
cd steam-gift-manager/
docker-compose down && docker-compose up -d --build
```
### 7. **Open the App**
Go to [http://localhost:5000](http://localhost:5000) in your browser. Go to [http://localhost:5000](http://localhost:5000) in your browser.
- Register your first user. - 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! - Enjoy search, status, and automatic Steam cover images!
--- ---
## 🛠️ Technology Stack 🛠️ ## 🛠️ Technology Stack 🛠️
- **Frontend:** Bootstrap 5, Jinja2 Templates - **Frontend:** Bootstrap 5, Jinja2 Templates ...
- **Backend:** Python 3, Flask, Flask-Babel, Flask-Login, Flask-SQLAlchemy - **Backend:** Python 3, Flask, Flask-Babel, Flask-Login, Flask-SQLAlchemy ...
- **Database:** SQLite (persisted in `data/`) - **Database:** SQLite (persisted in `data/`)
- **Containerization:** Docker, docker-compose - **Containerization:** Docker, docker-compose
- **Translations:** Flask-Babel, editable `.po` files in `steam-translations/` - **Translations:** Flask-Babel, editable `.po` files in `translations/`
## 🌍 Multi-language ## 🌍 Multi-language
- Switch between English and German using the dropdown in the navigation bar. - Switch between English and German using the dropdown in the navigation bar.
- All game and menu texts are translated. - All game and menu texts can be translated or individualized.
- You can add more languages by editing the `.po` files and running `./translate.sh`.
## 🔔 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! 🙌 ## 🙌 Contribute! 🙌
This project is open source and thrives on your help! This project is open source!
- **Bug Reports:** Please report bugs as Issues. - **Bug Reports:** Please report bugs as Issues.
- **Feature Requests:** Suggest new features! - **Feature Requests:** Suggest new features!
- **Pull Requests:** Submit your code changes! - **Pull Requests:** Submit your code changes!
// **only possible after Forgejo opens for federation** \\\
--- ---
## 📜 License 📜 ## 📜 License 📜
@ -118,4 +185,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!** 🚀

1294
setup.sh

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,14 @@
FROM python:3.10-slim FROM python:3.10-slim
# Shell explizit setzen
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-c"]
# Datenbankordner erstellen und Berechtigungen setzen 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 && chmod -R a+rwX /app/data
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 WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
@ -14,10 +18,10 @@ COPY . .
ARG UID=1000 ARG UID=1000
ARG GID=1000 ARG GID=1000
RUN groupadd -g $GID appuser && useradd -u $UID -g $GID -m appuser && chown -R appuser:appuser /app RUN groupadd -g $GID appuser && useradd -u $UID -g $GID -m appuser && chown -R appuser:appuser /app
USER appuser USER appuser
EXPOSE 5000 EXPOSE 5000
CMD ["python", "app.py"]
CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]

View File

@ -1,25 +1,90 @@
from flask import Flask, render_template, request, redirect, url_for, flash, make_response, session, abort 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_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_babel import Babel, _ from flask_babel import Babel, _
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime from datetime import datetime, timedelta
import os 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 = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24) csrf = CSRFProtect(app)
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'
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"
}
db = SQLAlchemy(app) metadata = MetaData(naming_convention=convention)
load_dotenv(override=True)
# Lade Umgebungsvariablen aus .env mit override
load_dotenv(override=True)
# App-Configuration
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))
# Initialisation
db = SQLAlchemy(app, metadata=metadata)
migrate = Migrate(app, db)
login_manager = LoginManager(app) login_manager = LoginManager(app)
login_manager.login_view = 'login' login_manager.login_view = 'login'
babel = Babel(app) babel = Babel(app)
# Logging
app.logger.addHandler(logging.StreamHandler())
app.logger.setLevel(logging.INFO)
@babel.localeselector @babel.localeselector
def get_locale(): def get_locale():
if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']: if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']:
@ -33,36 +98,63 @@ def inject_template_vars():
theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light' theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light'
) )
class User(UserMixin, db.Model): # DB Models
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), unique=True) username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(100)) password = db.Column(db.String(256), nullable=False)
games = db.relationship('Game', backref='owner', lazy=True) games = db.relationship('Game', back_populates='owner', lazy=True)
class Game(db.Model): class Game(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
owner = db.relationship('User', back_populates='games')
name = db.Column(db.String(100), nullable=False) 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) status = db.Column(db.String(50), nullable=False)
recipient = db.Column(db.String(100)) recipient = db.Column(db.String(100))
notes = db.Column(db.Text) notes = db.Column(db.Text)
url = db.Column(db.String(200)) url = db.Column(db.String(200))
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
redeem_date = db.Column(db.DateTime) 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)) 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 @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
return db.session.get(User, int(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('/') @app.route('/')
@login_required @login_required
def index(): def index():
search_query = request.args.get('q', '') 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: if search_query:
query = query.filter(Game.name.ilike(f'%{search_query}%')) query = query.filter(Game.name.ilike(f'%{search_query}%'))
games = query.order_by(Game.created_at.desc()).all() games = query.order_by(Game.created_at.desc()).all()
return render_template('index.html', return render_template('index.html',
games=games, games=games,
@ -87,25 +179,34 @@ def login():
username = request.form['username'] username = request.form['username']
password = request.form['password'] password = request.form['password']
user = User.query.filter_by(username=username).first() user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password): if user and check_password_hash(user.password, password):
login_user(user) login_user(user)
return redirect(url_for('index')) return redirect(url_for('index'))
flash(_('Invalid credentials'), 'danger') flash(_('Invalid credentials'), 'danger')
return render_template('login.html') return render_template('login.html')
@app.route('/register', methods=['GET', 'POST']) @app.route('/register', methods=['GET', 'POST'])
def register(): def register():
if not app.config['REGISTRATION_ENABLED']:
flash(_('Registrierungen sind deaktiviert'), 'danger')
return redirect(url_for('login'))
if request.method == 'POST': if request.method == 'POST':
username = request.form['username'] username = request.form['username']
password = generate_password_hash(request.form['password']) password = generate_password_hash(request.form['password'])
if User.query.filter_by(username=username).first(): if User.query.filter_by(username=username).first():
flash(_('Username already exists'), 'danger') flash(_('Username already exists'), 'danger')
return redirect(url_for('register')) return redirect(url_for('register'))
new_user = User(username=username, password=password) new_user = User(username=username, password=password)
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
login_user(new_user) login_user(new_user)
return redirect(url_for('index')) return redirect(url_for('index'))
return render_template('register.html') return render_template('register.html')
@app.route('/logout') @app.route('/logout')
@ -114,13 +215,28 @@ def logout():
logout_user() logout_user()
return redirect(url_for('login')) 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): if not check_password_hash(current_user.password, current_password):
match = re.search(r'store\.steampowered\.com/app/(\d+)', url or '') flash(_('Aktuelles Passwort ist falsch'), 'danger')
if match: return redirect(url_for('change_password'))
return match.group(1)
return '' 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']) @app.route('/add', methods=['GET', 'POST'])
@login_required @login_required
@ -129,8 +245,10 @@ def add_game():
try: try:
url = request.form.get('url', '') url = request.form.get('url', '')
steam_appid = request.form.get('steam_appid', '').strip() steam_appid = request.form.get('steam_appid', '').strip()
if not steam_appid: if not steam_appid:
steam_appid = extract_steam_appid(url) steam_appid = extract_steam_appid(url)
new_game = Game( new_game = Game(
name=request.form['name'], name=request.form['name'],
steam_key=request.form['steam_key'], steam_key=request.form['steam_key'],
@ -138,43 +256,57 @@ def add_game():
recipient=request.form.get('recipient', ''), recipient=request.form.get('recipient', ''),
notes=request.form.get('notes', ''), notes=request.form.get('notes', ''),
url=url, url=url,
steam_appid=steam_appid, # <- jetzt wird sie gesetzt! steam_appid=steam_appid,
redeem_date=datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None, redeem_date=datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None,
user_id=current_user.id user_id=current_user.id
) )
db.session.add(new_game) db.session.add(new_game)
db.session.commit() db.session.commit()
flash(_('Game added successfully!'), 'success') flash(_('Game added successfully!'), 'success')
return redirect(url_for('index')) return redirect(url_for('index'))
except IntegrityError:
db.session.rollback()
flash(_('Steam Key already exists!'), 'danger')
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
flash(_('Error: ') + str(e), 'danger') flash(_('Error: ') + str(e), 'danger')
return render_template('add_game.html')
return render_template('add_game.html')
@app.route('/edit/<int:game_id>', methods=['GET', 'POST']) @app.route('/edit/<int:game_id>', methods=['GET', 'POST'])
@login_required @login_required
def edit_game(game_id): def edit_game(game_id):
game = db.session.get(Game, game_id) # SQLAlchemy 2.x-kompatibel game = db.session.get(Game, game_id)
if not game or game.owner != current_user: 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': if request.method == 'POST':
try: try:
# Steam AppID aus Formular oder URL extrahieren
url = request.form.get('url', '') url = request.form.get('url', '')
steam_appid = request.form.get('steam_appid', '').strip() steam_appid = request.form.get('steam_appid', '').strip()
if not steam_appid: if not steam_appid:
steam_appid = extract_steam_appid(url) steam_appid = extract_steam_appid(url)
# Aktualisiere alle Felder
game.name = request.form['name'] game.name = request.form['name']
game.steam_key = request.form['steam_key'] game.steam_key = request.form['steam_key']
game.status = request.form['status'] game.status = request.form['status']
game.recipient = request.form.get('recipient', '') game.recipient = request.form.get('recipient', '')
game.notes = request.form.get('notes', '') game.notes = request.form.get('notes', '')
game.url = url game.url = url
game.steam_appid = steam_appid # <- FEHLTE HIER game.steam_appid = steam_appid
game.redeem_date = datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None game.redeem_date = datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None
db.session.commit() db.session.commit()
@ -187,26 +319,360 @@ def edit_game(game_id):
return render_template('edit_game.html', return render_template('edit_game.html',
game=game, game=game,
redeem_url=redeem_url,
active_redeem=active_redeem,
redeem_date=game.redeem_date.strftime('%Y-%m-%d') if game.redeem_date else '') redeem_date=game.redeem_date.strftime('%Y-%m-%d') if game.redeem_date else '')
@app.route('/delete/<int:game_id>', methods=['POST']) @app.route('/delete/<int:game_id>', methods=['POST'])
@login_required @login_required
def delete_game(game_id): 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: if game.owner != current_user:
return _("Not allowed!"), 403 abort(403)
try: try:
db.session.delete(game) db.session.delete(game)
db.session.commit() db.session.commit()
flash(_('Game deleted!'), 'success')
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
flash(_('Error deleting: ') + str(e), 'danger')
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/export', methods=['GET'])
@login_required
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.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')),
mimetype='text/csv',
as_attachment=True,
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 ''
])
# Table format
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"))
reader = csv.DictReader(stream)
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: cleaning up old 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 start
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 of the Schedulers when stopping the app
atexit.register(lambda: scheduler.shutdown())
if __name__ == '__main__': if __name__ == '__main__':
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
app.run(host='0.0.0.0', port=5000) app.run(host='0.0.0.0', port=5000)

View File

@ -1,2 +1,3 @@
[python: **.py] [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: services:
steam-manager: steam-manager:
build: . build: .
ports: ports:
- "5000:5000" - "5000:5000"
volumes:
- /root/test/data:/app/data
- /root/test/steam-translations:/app/translations
environment: environment:
- FLASK_DEBUG=0 - REGISTRATION_ENABLED=True
- TZ=
volumes:
- ../data:/app/data
- ../translations:/app/translations
- ../.env:/app/.env
user: "1000:1000"
restart: unless-stopped restart: unless-stopped

View File

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

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: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -31,3 +31,32 @@ body {
font-size: 0.9em; font-size: 0.9em;
font-weight: 500; 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-Animations */
#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"> <div class="card p-4 shadow-sm">
<h2 class="mb-4">{{ _('Add New Game') }}</h2> <h2 class="mb-4">{{ _('Add New Game') }}</h2>
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">{{ _('Name') }} *</label> <label class="form-label">{{ _('Name') }} *</label>
<input type="text" name="name" class="form-control" required> <input type="text" name="name" class="form-control" required>
</div> </div>
<div class="col-md-6"> <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> <input type="text" name="steam_key" class="form-control" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">

View File

@ -3,14 +3,18 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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 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') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg bg-body-tertiary"> <nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container"> <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"> <div class="d-flex align-items-center gap-3">
<form class="d-flex" action="{{ url_for('index') }}" method="GET"> <form class="d-flex" action="{{ url_for('index') }}" method="GET">
<input class="form-control me-2" <input class="form-control me-2"
@ -26,26 +30,22 @@
id="darkModeSwitch" {% if theme == 'dark' %}checked{% endif %}> id="darkModeSwitch" {% if theme == 'dark' %}checked{% endif %}>
<label class="form-check-label" for="darkModeSwitch">{{ _('Dark Mode') }}</label> <label class="form-check-label" for="darkModeSwitch">{{ _('Dark Mode') }}</label>
</div> </div>
<!-- Sprachumschalter -->
<div class="dropdown ms-3"> <div class="dropdown ms-3">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <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 %} {% if get_locale() == 'de' %} Deutsch {% elif get_locale() == 'en' %} English {% else %} Sprache {% endif %}
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li><a class="dropdown-item {% if get_locale() == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li>
<a class="dropdown-item {% if get_locale() == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}"> <li><a class="dropdown-item {% if get_locale() == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li>
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> </ul>
</div> </div>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<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 %} {% endif %}
</div> </div>
</div> </div>
@ -66,16 +66,16 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const toggle = document.getElementById('darkModeSwitch'); const toggle = document.getElementById('darkModeSwitch')
const html = document.documentElement; const html = document.documentElement
toggle.addEventListener('change', function() { toggle.addEventListener('change', function() {
const theme = this.checked ? 'dark' : 'light'; const theme = this.checked ? 'dark' : 'light'
fetch('/set-theme/' + theme) fetch('/set-theme/' + theme)
.then(() => { .then(() => html.setAttribute('data-bs-theme', theme))
html.setAttribute('data-bs-theme', theme); })
}); })
});
});
</script> </script>
{% include "footer.html" %}
</body> </body>
</html> </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,13 +3,14 @@
<div class="card p-4 shadow-sm"> <div class="card p-4 shadow-sm">
<h2 class="mb-4">{{ _('Edit Game') }}</h2> <h2 class="mb-4">{{ _('Edit Game') }}</h2>
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">{{ _('Name') }} *</label> <label class="form-label">{{ _('Name') }} *</label>
<input type="text" name="name" class="form-control" value="{{ game.name }}" required> <input type="text" name="name" class="form-control" value="{{ game.name }}" required>
</div> </div>
<div class="col-md-6"> <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> <input type="text" name="steam_key" class="form-control" value="{{ game.steam_key }}" required>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -40,6 +41,21 @@
<label class="form-label">{{ _('Notes') }}</label> <label class="form-label">{{ _('Notes') }}</label>
<textarea name="notes" class="form-control" rows="3">{{ game.notes }}</textarea> <textarea name="notes" class="form-control" rows="3">{{ game.notes }}</textarea>
</div> </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"> <div class="col-12">
<button type="submit" class="btn btn-primary">{{ _('Save') }}</button> <button type="submit" class="btn btn-primary">{{ _('Save') }}</button>
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a> <a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a>

View File

@ -0,0 +1,18 @@
<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://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

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block content %}
<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>
</div>
<button type="submit" class="btn btn-success">{{ _('Importieren') }}</button>
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Abbrechen') }}</a>
</form>
</div>
{% endblock %}

View File

@ -2,9 +2,12 @@
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1>{{ _('My Games') }}</h1> <h1>{{ _('My Games') }}</h1>
<a href="{{ url_for('add_game') }}" class="btn btn-primary"> <div>
+ {{ _('Add New Game') }} <a href="{{ url_for('export_games') }}" class="btn btn-outline-secondary">⬇️ {{ _('Export CSV') }}</a>
</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> </div>
{% if games %} {% if games %}
@ -54,8 +57,16 @@
{% endif %} {% endif %}
</td> </td>
<td class="text-nowrap"> <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> <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"> <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> <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ _('Really delete?') }}')">🗑️</button>
</form> </form>
</td> </td>
@ -64,6 +75,33 @@
</tbody> </tbody>
</table> </table>
</div> </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 %} {% else %}
<div class="alert alert-info">{{ _('No games yet') }}</div> <div class="alert alert-info">{{ _('No games yet') }}</div>
{% endif %} {% endif %}

View File

@ -3,9 +3,11 @@
<div class="row justify-content-center mt-5"> <div class="row justify-content-center mt-5">
<div class="col-md-6"> <div class="col-md-6">
<div class="card shadow-sm"> <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="266" height="206" class="mb-4" style="object-fit:contain;">
<h2 class="card-title mb-4">{{ _('Login') }}</h2> <h2 class="card-title mb-4">{{ _('Login') }}</h2>
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ _('Username') }}</label> <label class="form-label">{{ _('Username') }}</label>
<input type="text" name="username" class="form-control" required> <input type="text" name="username" class="form-control" required>
@ -24,3 +26,4 @@
</div> </div>
</div> </div>
{% endblock %} {% 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);
}
// run countdown
updateCountdown();
const timer = setInterval(updateCountdown, 1000);
</script>
{% endblock %}

View File

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

View File

@ -1,185 +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-21 11:24+0000\n"
"PO-Revision-Date: 2025-04-21 11:24+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:93
msgid "Invalid credentials"
msgstr "Ooops. Falsche Benutzerdaten!"
#: app.py:102
msgid "Username already exists"
msgstr "Benutzer existiert bereits"
#: app.py:147
msgid "Game added successfully!"
msgstr "Spiel erfolgreich hinzugefügt!"
#: app.py:151 app.py:186
msgid "Error: "
msgstr "Ui. Ein Fehler: "
#: app.py:160 app.py:198
msgid "Not allowed!"
msgstr "Das ist nicht erlaubt!"
#: app.py:181
msgid "Changes saved!"
msgstr "Änderungen gespeichert!"
#: app.py:202
msgid "Game deleted!"
msgstr "Spiel gelöscht!"
#: app.py:205
msgid "Error deleting: "
msgstr "Fehler beim Löschen: "
#: 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 "Steam Schlüssel"
#: 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 "Logout"
msgstr "Abmelden"
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr "Spiel editieren"
#: templates/edit_game.html:16
msgid "Steam AppID (optional)"
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 "Schlüssel"
#: templates/index.html:19
msgid "Created"
msgstr "Erstellt"
#: templates/index.html:21 templates/index.html:53
msgid "Shop"
msgstr "Shop"
#: 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 "Noch keine Spiele vorhanden"
#: 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 "Noch kein Account? Hier Registrieren!"
#: templates/register.html:7 templates/register.html:17
msgid "Register"
msgstr "Registrierung"

View File

@ -1,185 +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-21 11:24+0000\n"
"PO-Revision-Date: 2025-04-21 11:24+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:93
msgid "Invalid credentials"
msgstr ""
#: app.py:102
msgid "Username already exists"
msgstr ""
#: app.py:147
msgid "Game added successfully!"
msgstr ""
#: app.py:151 app.py:186
msgid "Error: "
msgstr ""
#: app.py:160 app.py:198
msgid "Not allowed!"
msgstr ""
#: app.py:181
msgid "Changes saved!"
msgstr ""
#: app.py:202
msgid "Game deleted!"
msgstr ""
#: app.py:205
msgid "Error deleting: "
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 "Logout"
msgstr ""
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr ""
#: templates/edit_game.html:16
msgid "Steam AppID (optional)"
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,184 +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-21 11:24+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:93
msgid "Invalid credentials"
msgstr ""
#: app.py:102
msgid "Username already exists"
msgstr ""
#: app.py:147
msgid "Game added successfully!"
msgstr ""
#: app.py:151 app.py:186
msgid "Error: "
msgstr ""
#: app.py:160 app.py:198
msgid "Not allowed!"
msgstr ""
#: app.py:181
msgid "Changes saved!"
msgstr ""
#: app.py:202
msgid "Game deleted!"
msgstr ""
#: app.py:205
msgid "Error deleting: "
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 "Logout"
msgstr ""
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr ""
#: templates/edit_game.html:16
msgid "Steam AppID (optional)"
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" cd "$(dirname "$0")/steam-gift-manager"
# 1. Extrahiere alle Texte declare -A locales=(
["de"]="de"
["en"]="en"
)
# create POT-file
docker-compose exec steam-manager pybabel extract -F babel.cfg -o translations/messages.pot . docker-compose exec steam-manager pybabel extract -F babel.cfg -o translations/messages.pot .
# 2. Initialisiere Sprachen (nur einmal nötig, danach auskommentieren) # Check for each language and initialize if necessary
for lang in de en; do for lang in "${!locales[@]}"; do
if [ ! -f "../steam-translations/$lang/LC_MESSAGES/messages.po" ]; then if [ ! -f "translations/${locales[$lang]}/LC_MESSAGES/messages.po" ]; then
docker-compose exec steam-manager pybabel init -i translations/messages.pot -d translations -l $lang docker-compose exec steam-manager pybabel init \
-i translations/messages.pot \
-d translations \
-l "${locales[$lang]}"
fi fi
done done
# 3. Aktualisiere Übersetzungen # Update and compile translations
docker-compose exec steam-manager pybabel update -i translations/messages.pot -d translations 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 docker-compose exec steam-manager pybabel compile -d translations
echo "✅ Übersetzungen extrahiert, aktualisiert und kompiliert!" echo "✅ Translations updated!"

View File

@ -0,0 +1,274 @@
# 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-29 15:53+0000\n"
"PO-Revision-Date: 2025-04-29 15:42+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:194
msgid "Invalid credentials"
msgstr "Ungültige Anmeldedaten"
#: app.py:200
msgid "No new registrations. They are deactivated!"
msgstr "Keine neuen Registrierungen. Sie sind deaktiviert!"
#: app.py:208
msgid "Username already exists"
msgstr "Benutzername existiert bereits"
#: app.py:234
msgid "Current passwort is wrong"
msgstr "Aktuelles Passwort ist falsch"
#: app.py:238
msgid "New Passwords are not matching"
msgstr "Neue Passwörter stimmen nicht überein"
#: app.py:243
msgid "Password changed successfully"
msgstr "Passwort erfolgreich geändert"
#: app.py:273
msgid "Game added successfully!"
msgstr "Spiel erfolgreich hinzugefügt!"
#: app.py:278
msgid "Steam Key already exists!"
msgstr "Steam-Key existiert bereits!"
#: app.py:281 app.py:325
msgid "Error: "
msgstr "Fehler: "
#: app.py:320
msgid "Changes saved!"
msgstr "Änderungen gespeichert!"
#: app.py:408
msgid "Game List (without Keys)"
msgstr "Spieleliste (ohne Keys)"
#: app.py:501
#, python-format
msgid "%(new)d new games imported, %(dup)d skipped duplicates"
msgstr "%(new)d neue Spiele importiert, %(dup)d Duplikate übersprungen"
#: app.py:505
#, python-format
msgid "Import error: %(error)s"
msgstr "Importfehler: %(error)s"
#: app.py:509
msgid "Please upload a valid CSV file."
msgstr "Bitte eine gültige CSV-Datei hochladen."
#: templates/add_game.html:4 templates/index.html:9
msgid "Add New Game"
msgstr "Neues Spiel hinzufügen"
#: templates/add_game.html:9 templates/edit_game.html:9 templates/index.html:19
msgid "Name"
msgstr "Name"
#: templates/add_game.html:13 templates/edit_game.html:13
msgid "Game Key"
msgstr "Spiele-Key"
#: templates/add_game.html:17 templates/edit_game.html:21 templates/index.html:21
msgid "Status"
msgstr "Status"
#: templates/add_game.html:19 templates/edit_game.html:23 templates/index.html:41
msgid "Not redeemed"
msgstr "Nicht eingelöst"
#: templates/add_game.html:20 templates/edit_game.html:24 templates/index.html:43
msgid "Gifted"
msgstr "Verschenkt"
#: templates/add_game.html:21 templates/edit_game.html:25 templates/index.html:45
msgid "Redeemed"
msgstr "Eingelöst"
#: templates/add_game.html:25 templates/edit_game.html:29 templates/index.html:23
msgid "Redeem by"
msgstr "Einzulösen bis"
#: templates/add_game.html:29 templates/edit_game.html:33
msgid "Recipient"
msgstr "Empfänger"
#: templates/add_game.html:33 templates/edit_game.html:37
msgid "Shop URL"
msgstr "Shop-URL"
#: templates/add_game.html:37 templates/edit_game.html:41
msgid "Notes"
msgstr "Notizen"
#: templates/add_game.html:41 templates/edit_game.html:60
msgid "Save"
msgstr "Speichern"
#: templates/add_game.html:42 templates/edit_game.html:61 templates/import.html:12
msgid "Cancel"
msgstr "Abbrechen"
#: templates/base.html:7
msgid "Game Key Manager"
msgstr "Game-Key-Verwaltung"
#: templates/base.html:23
msgid "Search"
msgstr "Suche"
#: templates/base.html:31
msgid "Dark Mode"
msgstr "Dunkler Modus"
#: templates/base.html:46 templates/login.html:16 templates/register.html:15
msgid "Password"
msgstr "Passwort"
#: templates/base.html:49
msgid "Logout"
msgstr "Abmelden"
#: templates/change_password.html:4 templates/change_password.html:19
msgid "Change Password"
msgstr "Passwort ändern"
#: templates/change_password.html:8
msgid "Current Password"
msgstr "Aktuelles Passwort"
#: templates/change_password.html:12
msgid "New Password"
msgstr "Neues Passwort"
#: templates/change_password.html:16
msgid "Confirm New Password"
msgstr "Neues Passwort bestätigen"
#: templates/edit_game.html:4
msgid "Edit Game"
msgstr "Spiel bearbeiten"
#: templates/edit_game.html:17
msgid "Steam AppID (optional)"
msgstr "Steam-AppID (optional)"
#: templates/edit_game.html:47
msgid "Active Redeem Link"
msgstr "Aktiver Einlöse-Link"
#: templates/edit_game.html:54
msgid "Expires at"
msgstr "Ablaufdatum"
#: templates/import.html:4
msgid "Import Games"
msgstr "Spiele importieren"
#: templates/import.html:8
msgid "Select CSV file"
msgstr "CSV-Datei auswählen"
#: templates/import.html:11
msgid "Import"
msgstr "Importieren"
#: templates/index.html:4
msgid "My Games"
msgstr "Meine Spiele"
#: templates/index.html:6
msgid "Export CSV"
msgstr "CSV exportieren"
#: templates/index.html:8
msgid "Import CSV"
msgstr "CSV importieren"
#: templates/index.html:18
msgid "Cover"
msgstr "Cover"
#: templates/index.html:20
msgid "Key"
msgstr "Key"
#: templates/index.html:22
msgid "Created"
msgstr "Erstellt"
#: templates/index.html:24 templates/index.html:56
msgid "Shop"
msgstr "Shop"
#: templates/index.html:25
msgid "Actions"
msgstr "Aktionen"
#: templates/index.html:63
msgid "Generate redeem link"
msgstr "Einlöse-Link generieren"
#: templates/index.html:70
msgid "Really delete?"
msgstr "Wirklich löschen?"
#: templates/index.html:96
msgid "Redeem link copied to clipboard!"
msgstr "Einlöse-Link in die Zwischenablage kopiert!"
#: templates/index.html:100
msgid "Error generating link"
msgstr "Fehler beim Generieren des Links"
#: templates/index.html:106
msgid "No games yet"
msgstr "Der Kornspeicher ist leer, Sire!"
#: templates/login.html:8 templates/login.html:19
msgid "Login"
msgstr "Anmelden"
#: templates/login.html:12 templates/register.html:11
msgid "Username"
msgstr "Benutzername"
#: templates/login.html:22
msgid "No account yet? Register"
msgstr "Noch kein Konto? Jetzt registrieren"
#: templates/redeem.html:16
msgid "Your Key:"
msgstr "Dein Key:"
#: templates/redeem.html:22
msgid "Redeem now on"
msgstr "Jetzt einlösen bei"
#: templates/redeem.html:26
msgid "This page will expire in"
msgstr "Diese Seite läuft ab in"
#: templates/register.html:7 templates/register.html:18
msgid "Register"
msgstr "Registrieren"

View File

@ -0,0 +1,280 @@
# 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-29 15:53+0000\n"
"PO-Revision-Date: 2025-04-29 15:42+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:194
msgid "Invalid credentials"
msgstr ""
#: app.py:200
msgid "No new registrations. They are deactivated!"
msgstr ""
#: app.py:208
msgid "Username already exists"
msgstr ""
#: app.py:234
msgid "Current passwort is wrong"
msgstr ""
#: app.py:238
msgid "New Passwords are not matching"
msgstr ""
#: app.py:243
msgid "Password changed successfully"
msgstr ""
#: app.py:273
msgid "Game added successfully!"
msgstr ""
#: app.py:278
msgid "Steam Key already exists!"
msgstr ""
#: app.py:281 app.py:325
msgid "Error: "
msgstr ""
#: app.py:320
msgid "Changes saved!"
msgstr ""
#: app.py:408
msgid "Game List (without Keys)"
msgstr ""
#: app.py:501
#, python-format
msgid "%(new)d new games imported, %(dup)d skipped duplicates"
msgstr ""
#: app.py:505
#, python-format
msgid "Import error: %(error)s"
msgstr ""
#: app.py:509
msgid "Please upload a valid CSV file."
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
#: templates/import.html:12
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:46 templates/login.html:16 templates/register.html:15
msgid "Password"
msgstr ""
#: templates/base.html:49
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 "Select CSV file"
msgstr ""
#: templates/import.html:11
msgid "Import"
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: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 ""

279
translations/messages.pot Normal file
View File

@ -0,0 +1,279 @@
# 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-29 15:53+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:194
msgid "Invalid credentials"
msgstr ""
#: app.py:200
msgid "No new registrations. They are deactivated!"
msgstr ""
#: app.py:208
msgid "Username already exists"
msgstr ""
#: app.py:234
msgid "Current passwort is wrong"
msgstr ""
#: app.py:238
msgid "New Passwords are not matching"
msgstr ""
#: app.py:243
msgid "Password changed successfully"
msgstr ""
#: app.py:273
msgid "Game added successfully!"
msgstr ""
#: app.py:278
msgid "Steam Key already exists!"
msgstr ""
#: app.py:281 app.py:325
msgid "Error: "
msgstr ""
#: app.py:320
msgid "Changes saved!"
msgstr ""
#: app.py:408
msgid "Game List (without Keys)"
msgstr ""
#: app.py:501
#, python-format
msgid "%(new)d new games imported, %(dup)d skipped duplicates"
msgstr ""
#: app.py:505
#, python-format
msgid "Import error: %(error)s"
msgstr ""
#: app.py:509
msgid "Please upload a valid CSV file."
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
#: templates/import.html:12
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:46 templates/login.html:16 templates/register.html:15
msgid "Password"
msgstr ""
#: templates/base.html:49
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 "Select CSV file"
msgstr ""
#: templates/import.html:11
msgid "Import"
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: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
# Set the working directory to the project directory
cd "$(dirname "$0")/steam-gift-manager"
# Setze FLASK_APP, falls nötig
export FLASK_APP=app.py
# Initialize migrations, if not yet available
if [ ! -d migrations ]; then
echo "Starting Flask-Migrate..."
docker-compose exec steam-manager flask db init
fi
# Create migration (only if models have changed)
docker-compose exec steam-manager flask db migrate -m "Automatic Migration"
# Apply migration
docker-compose exec steam-manager flask db upgrade
echo "✅ Database migration completed!"