Compare commits
	
		
			9 commits
		
	
	
		
			d9bca6f24c
			...
			7bf20396e6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7bf20396e6 | |||
| 380376c16b | |||
| 3c5b554de1 | |||
| f2bd4cdbb4 | |||
| f5dd3483f7 | |||
| 6182022222 | |||
| db2a6556eb | |||
| 61c2a1dcd7 | |||
| eaa508a8df | 
					 30 changed files with 2785 additions and 994 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								GameManager.png
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								GameManager.png
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 190 KiB  | 
							
								
								
									
										97
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										97
									
								
								README.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,34 +1,50 @@
 | 
			
		|||
# 🔑 Game Key Management System 🔑
 | 
			
		||||
# 🔑 Game Key Manager 🔑
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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)
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## ✨ 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
 | 
			
		||||
# Security
 | 
			
		||||
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 you’d like to support itme, you can make a donation here:
 | 
			
		||||
 | 
			
		||||
[](https://ko-fi.com/nocci)
 | 
			
		||||
 | 
			
		||||
[](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!** 🚀
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 .
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,3 @@
 | 
			
		|||
[python: **.py]
 | 
			
		||||
[jinja2: templates/**.html]
 | 
			
		||||
[jinja2: **/templates/**.html]
 | 
			
		||||
extensions=jinja2.ext.autoescape,jinja2.ext.with_
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 52 KiB  | 
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								steam-gift-manager/templates/change_password.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								steam-gift-manager/templates/change_password.html
									
										
									
									
									
										Normal 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 %}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								steam-gift-manager/templates/footer.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								steam-gift-manager/templates/footer.html
									
										
									
									
									
										Normal 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> — 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> ·
 | 
			
		||||
      <a href="https://liberapay.com/nocci" target="_blank" rel="noopener">Liberapay</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</footer>
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 %}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										91
									
								
								steam-gift-manager/templates/redeem.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								steam-gift-manager/templates/redeem.html
									
										
									
									
									
										Normal 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 %}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			@ -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 ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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 ""
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								translate.sh
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								translate.sh
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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!"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										287
									
								
								translations/de/LC_MESSAGES/messages.po
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								translations/de/LC_MESSAGES/messages.po
									
										
									
									
									
										Normal 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 ""
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										287
									
								
								translations/en/LC_MESSAGES/messages.po
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								translations/en/LC_MESSAGES/messages.po
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										286
									
								
								translations/messages.pot
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										22
									
								
								upgrade.sh
									
										
									
									
									
										Normal 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!"
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue