Merge branch 'dev'
This commit is contained in:
		
						commit
						380376c16b
					
				
					 32 changed files with 2786 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.   | This project helps you keep track of your collected game keys.   | ||||||
| No more confusion about whether a key is redeemed, gifted, or still unused – now you have everything in one place, with search, status, and even automatic Steam cover images! | No more confusion about whether a key is redeemed, gifted, or still unused – now you have everything in one place, with search, status, and even automatic Steam cover images! | ||||||
| 
 | 
 | ||||||
|  | It's even possible to gift your keys via a unique website. Just edit the game to "Gifted" and you'll get a option to copy the on your overview page. (maybe HTTPS only) | ||||||
|  | 
 | ||||||
|  | (the link will also remain in the edit area) | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | 
 | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| ## ✨ Features ✨ | ## ✨ Features ✨ | ||||||
| 
 | 
 | ||||||
| - **Key Management:**   | - **Key Management:**   | ||||||
|   Enter your game keys, the corresponding game, platform, and where you got the key. |   Enter your game keys, the corresponding game, platform, and maybe where you got the key. | ||||||
| - **Status Tracking:**   | - **Status Tracking:**   | ||||||
|   Mark keys as "Redeemed", "Gifted" or "Available" – always know your status. |   Mark keys as "Redeemed", "Gifted" or "Available" – always know your status. | ||||||
| - **Shop URL & Steam Cover:**   | - **Shop URL & Steam Cover:**   | ||||||
|   Save the shop URL and (optionally) the Steam AppID. The app will automatically show the official Steam cover image if available. |   Save the shop URL and (optionally) the Steam AppID. The app will automatically show the official Steam cover image if available. | ||||||
|  | - **Gift your Games:** | ||||||
|  |   You can create a unique redeem/gift website, which will expire after 24h. | ||||||
| - **Multi-user:**   | - **Multi-user:**   | ||||||
|   Each user manages their own keys. |   Each user manages their own keys. | ||||||
| - **Search & Filter:**   | - **Enable/Disable Registrations:** | ||||||
|  |   Perfect if you want to run the Server just on your own (via .env file) | ||||||
|  | - **Search:**   | ||||||
|   Find games quickly with the search function. |   Find games quickly with the search function. | ||||||
| - **Responsive UI:**   | - **Responsive UI:**   | ||||||
|   Works on desktop and mobile, with Dark Mode toggle. |   Works on desktop and mobile, with Dark Mode toggle. | ||||||
| - **Multi-language:**   | - **Multi-language:**   | ||||||
|   Switch between English and German instantly. |   Switch between English and German instantly*. | ||||||
|  | - **Import/Export (CSV / PDF -only export-):** | ||||||
|  |   Easy export and import of your keys. (e.g. in case you have to start over) | ||||||
|  | - **Change Password:** | ||||||
|  |   Change your Password on the fly. | ||||||
|  | - **Website Security:** | ||||||
|  |   You can turn on/off CSRF and Secure Cookie via .env file. | ||||||
|  | - **Notifications:** | ||||||
|  |   If you have key that have to be redeemed before a specific date. You can set up sending messages via, Pushover, Matrix and Gotify | ||||||
| - **No key data leaves your server!** | - **No key data leaves your server!** | ||||||
| - **(Planned):** | - **(Planned):** | ||||||
|   - Import/Export (CSV, JSON) |   - ~~Import/Export (CSV)~~ | ||||||
|   - Redeem site with unique sharing link |   - ~~Redeem site with unique sharing link~~ | ||||||
| 
 | 
 | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
|  | @ -37,13 +53,14 @@ No more confusion about whether a key is redeemed, gifted, or still unused – n | ||||||
| ### 1. **Clone the Repository** | ### 1. **Clone the Repository** | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| git clone https://git.nocci.it/nocci/GiftGamesDB | git clone https://git.nocci.it/nocci/GameKeyManager | ||||||
| cd steam-gift-manager | cd steam-gift-manager | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### 2. **Setup Docker** | ### 2. **Setup Docker** | ||||||
| 
 | 
 | ||||||
| Make sure you have [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) installed. | Make sure you have [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) installed. | ||||||
|  | If not, the script will ask you what to do and can install Docker and docker-compose for you. (maybe not if you are running Arch) | ||||||
| 
 | 
 | ||||||
| ### 3. **Initial Setup** | ### 3. **Initial Setup** | ||||||
| 
 | 
 | ||||||
|  | @ -62,54 +79,90 @@ docker-compose build --no-cache | ||||||
| docker-compose up -d | docker-compose up -d | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### 5. **Initialize and Edit Translations (Optional)** | ### 5. **Edit your .env file to your liking** | ||||||
|  | 
 | ||||||
|  | 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 | ```bash | ||||||
| ./translate.sh | ./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 | ```bash | ||||||
| ./translate.sh | ./translate.sh | ||||||
| cd steam-gift-manager/ | 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. | Go to [http://localhost:5000](http://localhost:5000) in your browser. | ||||||
| 
 | 
 | ||||||
| - Register your first user. | - Register your first user. | ||||||
| - Add your keys, shop URLs, and (optionally) Steam AppIDs. | - Add your keys, shop URLs etc. | ||||||
| - Enjoy search, status, and automatic Steam cover images! | - Enjoy search, status, and automatic Steam cover images! | ||||||
| 
 | 
 | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| ## 🛠️ Technology Stack 🛠️ | ## 🛠️ Technology Stack 🛠️ | ||||||
| 
 | 
 | ||||||
| - **Frontend:** Bootstrap 5, Jinja2 Templates | - **Frontend:** Bootstrap 5, Jinja2 Templates ... | ||||||
| - **Backend:** Python 3, Flask, Flask-Babel, Flask-Login, Flask-SQLAlchemy | - **Backend:** Python 3, Flask, Flask-Babel, Flask-Login, Flask-SQLAlchemy ... | ||||||
| - **Database:** SQLite (persisted in `data/`) | - **Database:** SQLite (persisted in `data/`) | ||||||
| - **Containerization:** Docker, docker-compose | - **Containerization:** Docker, docker-compose | ||||||
| - **Translations:** Flask-Babel, editable `.po` files in `steam-translations/` | - **Translations:** Flask-Babel, editable `.po` files in `translations/` | ||||||
| 
 | 
 | ||||||
| ## 🌍 Multi-language | ## 🌍 Multi-language | ||||||
| 
 | 
 | ||||||
| - Switch between English and German using the dropdown in the navigation bar. | - Switch between English and German using the dropdown in the navigation bar. | ||||||
| - All game and menu texts are translated. | - All game and menu texts can be translated or individualized. | ||||||
| - You can add more languages by editing the `.po` files and running `./translate.sh`. | 
 | ||||||
|  | ## 🔔 Notifications | ||||||
|  | 
 | ||||||
|  | - Send notifications if a game has to be redeemed by a specific date | ||||||
|  | - Gotify, Matrix and Pushover are already available - have a look into the .env file | ||||||
|  | - 48 hours before you are running out of time the app will send you a notice | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ## 🪙 Do you this project? 🪙 | ||||||
|  | 
 | ||||||
|  | If 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! 🙌 | ## 🙌 Contribute! 🙌 | ||||||
| 
 | 
 | ||||||
| This project is open source and thrives on your help! | This project is open source! | ||||||
| 
 | 
 | ||||||
| - **Bug Reports:** Please report bugs as Issues. | - **Bug Reports:** Please report bugs as Issues. | ||||||
| - **Feature Requests:** Suggest new features! | - **Feature Requests:** Suggest new features! | ||||||
| - **Pull Requests:** Submit your code changes! | - **Pull Requests:** Submit your code changes! | ||||||
| 
 | 
 | ||||||
|  | // **only possible after Forgejo opens for federation** \\\ | ||||||
|  | 
 | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| ## 📜 License 📜 | ## 📜 License 📜 | ||||||
|  | @ -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"] | 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 | WORKDIR /app | ||||||
| COPY requirements.txt . | 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_sqlalchemy import SQLAlchemy | ||||||
| from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user | from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user | ||||||
| from flask_babel import Babel, _ | from flask_babel import Babel, _ | ||||||
| from werkzeug.security import generate_password_hash, check_password_hash | from werkzeug.security import generate_password_hash, check_password_hash | ||||||
| from datetime import datetime | from datetime import datetime, timedelta | ||||||
| import os | from flask_wtf import CSRFProtect | ||||||
|  | from flask import abort | ||||||
|  | import io | ||||||
|  | import warnings | ||||||
|  | import re | ||||||
| import io | import io | ||||||
| import csv | import csv | ||||||
|  | import secrets | ||||||
|  | import requests | ||||||
|  | from dotenv import load_dotenv | ||||||
|  | load_dotenv(override=True) | ||||||
|  | from sqlalchemy.exc import IntegrityError | ||||||
|  | from apscheduler.schedulers.background import BackgroundScheduler | ||||||
|  | import atexit | ||||||
|  | from flask_migrate import Migrate | ||||||
|  | from sqlalchemy import MetaData | ||||||
|  | from reportlab.pdfgen import canvas | ||||||
|  | from reportlab.lib.pagesizes import A4, landscape, letter | ||||||
|  | from reportlab.platypus import ( | ||||||
|  |     SimpleDocTemplate,  | ||||||
|  |     Table,  | ||||||
|  |     TableStyle,  | ||||||
|  |     Paragraph,  | ||||||
|  |     Image,  | ||||||
|  |     Spacer | ||||||
|  | ) | ||||||
|  | from reportlab.lib import colors | ||||||
|  | from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | ||||||
|  | from reportlab.lib.utils import ImageReader | ||||||
|  | from reportlab.lib.units import cm, inch, mm | ||||||
|  | from io import BytesIO | ||||||
|  | import reportlab.lib | ||||||
| 
 | 
 | ||||||
| app = Flask(__name__) | app = Flask(__name__) | ||||||
| app.config['SECRET_KEY'] = os.urandom(24) | csrf = CSRFProtect(app) | ||||||
| app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/data/games.db' |  | ||||||
| app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False |  | ||||||
| app.config['BABEL_DEFAULT_LOCALE'] = 'de' |  | ||||||
| app.config['BABEL_SUPPORTED_LOCALES'] = ['de', 'en'] |  | ||||||
| app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations' |  | ||||||
| 
 | 
 | ||||||
| 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 = LoginManager(app) | ||||||
| login_manager.login_view = 'login' | login_manager.login_view = 'login' | ||||||
| babel = Babel(app) | babel = Babel(app) | ||||||
| 
 | 
 | ||||||
|  | # Logging | ||||||
|  | app.logger.addHandler(logging.StreamHandler()) | ||||||
|  | app.logger.setLevel(logging.INFO) | ||||||
|  | 
 | ||||||
| @babel.localeselector | @babel.localeselector | ||||||
| def get_locale(): | def get_locale(): | ||||||
|     if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']: |     if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']: | ||||||
|  | @ -34,36 +98,63 @@ def inject_template_vars(): | ||||||
|         theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light' |         theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light' | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| class User(UserMixin, db.Model): | # Datenbankmodelle | ||||||
|  | class User(db.Model, UserMixin): | ||||||
|  |     __tablename__ = 'users' | ||||||
|     id = db.Column(db.Integer, primary_key=True) |     id = db.Column(db.Integer, primary_key=True) | ||||||
|     username = db.Column(db.String(100), unique=True) |     username = db.Column(db.String(80), unique=True, nullable=False) | ||||||
|     password = db.Column(db.String(100)) |     password = db.Column(db.String(256), nullable=False) | ||||||
|     games = db.relationship('Game', backref='owner', lazy=True) |     games = db.relationship('Game', back_populates='owner', lazy=True) | ||||||
| 
 | 
 | ||||||
| class Game(db.Model): | class Game(db.Model): | ||||||
|     id = db.Column(db.Integer, primary_key=True) |     id = db.Column(db.Integer, primary_key=True) | ||||||
|  |     owner = db.relationship('User', back_populates='games') | ||||||
|     name = db.Column(db.String(100), nullable=False) |     name = db.Column(db.String(100), nullable=False) | ||||||
|     steam_key = db.Column(db.String(100), nullable=False) |     steam_key = db.Column(db.String(100), nullable=False, unique=True) | ||||||
|     status = db.Column(db.String(50), nullable=False) |     status = db.Column(db.String(50), nullable=False) | ||||||
|     recipient = db.Column(db.String(100)) |     recipient = db.Column(db.String(100)) | ||||||
|     notes = db.Column(db.Text) |     notes = db.Column(db.Text) | ||||||
|     url = db.Column(db.String(200)) |     url = db.Column(db.String(200)) | ||||||
|     created_at = db.Column(db.DateTime, default=datetime.utcnow) |     created_at = db.Column(db.DateTime, default=datetime.utcnow) | ||||||
|     redeem_date = db.Column(db.DateTime) |     redeem_date = db.Column(db.DateTime) | ||||||
|     user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) |     user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) | ||||||
|     steam_appid = db.Column(db.String(20)) |     steam_appid = db.Column(db.String(20)) | ||||||
| 
 | 
 | ||||||
|  | class RedeemToken(db.Model): | ||||||
|  |     id = db.Column(db.Integer, primary_key=True) | ||||||
|  |     token = db.Column(db.String(17), unique=True, nullable=False) | ||||||
|  |     game_id = db.Column(db.Integer, db.ForeignKey('game.id'), nullable=False) | ||||||
|  |     expires = db.Column(db.DateTime, nullable=False) | ||||||
|  |     used = db.Column(db.Boolean, default=False) | ||||||
|  |     total_hours = db.Column(db.Integer, nullable=False) | ||||||
|  | 
 | ||||||
|  | with app.app_context(): | ||||||
|  |     db.create_all() | ||||||
|  | 
 | ||||||
| @login_manager.user_loader | @login_manager.user_loader | ||||||
| def load_user(user_id): | def load_user(user_id): | ||||||
|     return db.session.get(User, int(user_id)) |     return db.session.get(User, int(user_id)) | ||||||
| 
 | 
 | ||||||
|  | def extract_steam_appid(url): | ||||||
|  |     match = re.search(r'store\.steampowered\.com/app/(\d+)', url or '') | ||||||
|  |     return match.group(1) if match else '' | ||||||
|  | 
 | ||||||
|  | # 404  | ||||||
|  | def get_or_404(model, id): | ||||||
|  |     instance = db.session.get(model, id) | ||||||
|  |     if not instance: | ||||||
|  |         abort(404) | ||||||
|  |     return instance | ||||||
|  | 
 | ||||||
| @app.route('/') | @app.route('/') | ||||||
| @login_required | @login_required | ||||||
| def index(): | def index(): | ||||||
|     search_query = request.args.get('q', '') |     search_query = request.args.get('q', '') | ||||||
|     query = db.session.query(Game).filter_by(user_id=current_user.id) |     query = Game.query.filter_by(user_id=current_user.id) | ||||||
|  |      | ||||||
|     if search_query: |     if search_query: | ||||||
|         query = query.filter(Game.name.ilike(f'%{search_query}%')) |         query = query.filter(Game.name.ilike(f'%{search_query}%')) | ||||||
|  |      | ||||||
|     games = query.order_by(Game.created_at.desc()).all() |     games = query.order_by(Game.created_at.desc()).all() | ||||||
|     return render_template('index.html', |     return render_template('index.html', | ||||||
|                          games=games, |                          games=games, | ||||||
|  | @ -88,25 +179,34 @@ def login(): | ||||||
|         username = request.form['username'] |         username = request.form['username'] | ||||||
|         password = request.form['password'] |         password = request.form['password'] | ||||||
|         user = User.query.filter_by(username=username).first() |         user = User.query.filter_by(username=username).first() | ||||||
|  |          | ||||||
|         if user and check_password_hash(user.password, password): |         if user and check_password_hash(user.password, password): | ||||||
|             login_user(user) |             login_user(user) | ||||||
|             return redirect(url_for('index')) |             return redirect(url_for('index')) | ||||||
|  |          | ||||||
|         flash(_('Invalid credentials'), 'danger') |         flash(_('Invalid credentials'), 'danger') | ||||||
|     return render_template('login.html') |     return render_template('login.html') | ||||||
| 
 | 
 | ||||||
| @app.route('/register', methods=['GET', 'POST']) | @app.route('/register', methods=['GET', 'POST']) | ||||||
| def register(): | def register(): | ||||||
|  |     if not app.config['REGISTRATION_ENABLED']: | ||||||
|  |         flash(_('Registrierungen sind deaktiviert'), 'danger') | ||||||
|  |         return redirect(url_for('login')) | ||||||
|  |          | ||||||
|     if request.method == 'POST': |     if request.method == 'POST': | ||||||
|         username = request.form['username'] |         username = request.form['username'] | ||||||
|         password = generate_password_hash(request.form['password']) |         password = generate_password_hash(request.form['password']) | ||||||
|  |          | ||||||
|         if User.query.filter_by(username=username).first(): |         if User.query.filter_by(username=username).first(): | ||||||
|             flash(_('Username already exists'), 'danger') |             flash(_('Username already exists'), 'danger') | ||||||
|             return redirect(url_for('register')) |             return redirect(url_for('register')) | ||||||
|  |          | ||||||
|         new_user = User(username=username, password=password) |         new_user = User(username=username, password=password) | ||||||
|         db.session.add(new_user) |         db.session.add(new_user) | ||||||
|         db.session.commit() |         db.session.commit() | ||||||
|         login_user(new_user) |         login_user(new_user) | ||||||
|         return redirect(url_for('index')) |         return redirect(url_for('index')) | ||||||
|  |      | ||||||
|     return render_template('register.html') |     return render_template('register.html') | ||||||
| 
 | 
 | ||||||
| @app.route('/logout') | @app.route('/logout') | ||||||
|  | @ -115,13 +215,28 @@ def logout(): | ||||||
|     logout_user() |     logout_user() | ||||||
|     return redirect(url_for('login')) |     return redirect(url_for('login')) | ||||||
| 
 | 
 | ||||||
| import re | @app.route('/change-password', methods=['GET', 'POST']) | ||||||
|  | @login_required | ||||||
|  | def change_password(): | ||||||
|  |     if request.method == 'POST': | ||||||
|  |         current_password = request.form['current_password'] | ||||||
|  |         new_password = request.form['new_password'] | ||||||
|  |         confirm_password = request.form['confirm_password'] | ||||||
| 
 | 
 | ||||||
| def extract_steam_appid(url): |         if not check_password_hash(current_user.password, current_password): | ||||||
|     match = re.search(r'store\.steampowered\.com/app/(\d+)', url or '') |             flash(_('Aktuelles Passwort ist falsch'), 'danger') | ||||||
|     if match: |             return redirect(url_for('change_password')) | ||||||
|         return match.group(1) | 
 | ||||||
|     return '' |         if new_password != confirm_password: | ||||||
|  |             flash(_('Neue Passwörter stimmen nicht überein'), 'danger') | ||||||
|  |             return redirect(url_for('change_password')) | ||||||
|  | 
 | ||||||
|  |         current_user.password = generate_password_hash(new_password) | ||||||
|  |         db.session.commit() | ||||||
|  |         flash(_('Passwort erfolgreich geändert'), 'success') | ||||||
|  |         return redirect(url_for('index')) | ||||||
|  | 
 | ||||||
|  |     return render_template('change_password.html') | ||||||
| 
 | 
 | ||||||
| @app.route('/add', methods=['GET', 'POST']) | @app.route('/add', methods=['GET', 'POST']) | ||||||
| @login_required | @login_required | ||||||
|  | @ -130,8 +245,10 @@ def add_game(): | ||||||
|         try: |         try: | ||||||
|             url = request.form.get('url', '') |             url = request.form.get('url', '') | ||||||
|             steam_appid = request.form.get('steam_appid', '').strip() |             steam_appid = request.form.get('steam_appid', '').strip() | ||||||
|  |              | ||||||
|             if not steam_appid: |             if not steam_appid: | ||||||
|                 steam_appid = extract_steam_appid(url) |                 steam_appid = extract_steam_appid(url) | ||||||
|  |              | ||||||
|             new_game = Game( |             new_game = Game( | ||||||
|                 name=request.form['name'], |                 name=request.form['name'], | ||||||
|                 steam_key=request.form['steam_key'], |                 steam_key=request.form['steam_key'], | ||||||
|  | @ -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, |                 redeem_date=datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None, | ||||||
|                 user_id=current_user.id |                 user_id=current_user.id | ||||||
|             ) |             ) | ||||||
|  |              | ||||||
|             db.session.add(new_game) |             db.session.add(new_game) | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|             flash(_('Game added successfully!'), 'success') |             flash(_('Game added successfully!'), 'success') | ||||||
|             return redirect(url_for('index')) |             return redirect(url_for('index')) | ||||||
|  |          | ||||||
|  |         except IntegrityError: | ||||||
|  |             db.session.rollback() | ||||||
|  |             flash(_('Steam Key already exists!'), 'danger') | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             db.session.rollback() |             db.session.rollback() | ||||||
|             flash(_('Error: ') + str(e), 'danger') |             flash(_('Error: ') + str(e), 'danger') | ||||||
|  |      | ||||||
|     return render_template('add_game.html') |     return render_template('add_game.html') | ||||||
| 
 | 
 | ||||||
| @app.route('/edit/<int:game_id>', methods=['GET', 'POST']) | @app.route('/edit/<int:game_id>', methods=['GET', 'POST']) | ||||||
|  | @ -157,14 +280,26 @@ def add_game(): | ||||||
| def edit_game(game_id): | def edit_game(game_id): | ||||||
|     game = db.session.get(Game, game_id) |     game = db.session.get(Game, game_id) | ||||||
|     if not game or game.owner != current_user: |     if not game or game.owner != current_user: | ||||||
|         return _("Not allowed!"), 403 |         abort(404) | ||||||
|  |      | ||||||
|  |     if not game or game.owner != current_user: | ||||||
|  |         abort(403) | ||||||
|  |      | ||||||
|  |     active_redeem = RedeemToken.query.filter( | ||||||
|  |         RedeemToken.game_id == game_id, | ||||||
|  |         RedeemToken.expires > datetime.utcnow() | ||||||
|  |     ).first() | ||||||
|  |      | ||||||
|  |     redeem_url = url_for('redeem_page', token=active_redeem.token, _external=True) if active_redeem else None | ||||||
| 
 | 
 | ||||||
|     if request.method == 'POST': |     if request.method == 'POST': | ||||||
|         try: |         try: | ||||||
|             url = request.form.get('url', '') |             url = request.form.get('url', '') | ||||||
|             steam_appid = request.form.get('steam_appid', '').strip() |             steam_appid = request.form.get('steam_appid', '').strip() | ||||||
|  |              | ||||||
|             if not steam_appid: |             if not steam_appid: | ||||||
|                 steam_appid = extract_steam_appid(url) |                 steam_appid = extract_steam_appid(url) | ||||||
|  |              | ||||||
|             game.name = request.form['name'] |             game.name = request.form['name'] | ||||||
|             game.steam_key = request.form['steam_key'] |             game.steam_key = request.form['steam_key'] | ||||||
|             game.status = request.form['status'] |             game.status = request.form['status'] | ||||||
|  | @ -173,32 +308,40 @@ def edit_game(game_id): | ||||||
|             game.url = url |             game.url = url | ||||||
|             game.steam_appid = steam_appid |             game.steam_appid = steam_appid | ||||||
|             game.redeem_date = datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None |             game.redeem_date = datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None | ||||||
|  |              | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|             flash(_('Changes saved!'), 'success') |             flash(_('Changes saved!'), 'success') | ||||||
|             return redirect(url_for('index')) |             return redirect(url_for('index')) | ||||||
|  |          | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             db.session.rollback() |             db.session.rollback() | ||||||
|             flash(_('Error: ') + str(e), 'danger') |             flash(_('Error: ') + str(e), 'danger') | ||||||
|  |      | ||||||
|     return render_template('edit_game.html', |     return render_template('edit_game.html', | ||||||
|                           game=game, |                          game=game, | ||||||
|                           redeem_date=game.redeem_date.strftime('%Y-%m-%d') if game.redeem_date else '') |                          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']) | @app.route('/delete/<int:game_id>', methods=['POST']) | ||||||
| @login_required | @login_required | ||||||
| def delete_game(game_id): | def delete_game(game_id): | ||||||
|     game = Game.query.get_or_404(game_id) |     game = db.session.get(Game, game_id) | ||||||
|  |     if not game or game.owner != current_user: | ||||||
|  |         abort(404) | ||||||
|  |      | ||||||
|     if game.owner != current_user: |     if game.owner != current_user: | ||||||
|         return _("Not allowed!"), 403 |         abort(403) | ||||||
|  |      | ||||||
|     try: |     try: | ||||||
|         db.session.delete(game) |         db.session.delete(game) | ||||||
|         db.session.commit() |         db.session.commit() | ||||||
|         flash(_('Game deleted!'), 'success') |  | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         db.session.rollback() |         db.session.rollback() | ||||||
|         flash(_('Error deleting: ') + str(e), 'danger') |      | ||||||
|     return redirect(url_for('index')) |     return redirect(url_for('index')) | ||||||
| 
 | 
 | ||||||
| # --- Import/Export Funktionen --- |  | ||||||
| 
 | 
 | ||||||
| @app.route('/export', methods=['GET']) | @app.route('/export', methods=['GET']) | ||||||
| @login_required | @login_required | ||||||
|  | @ -206,14 +349,22 @@ def export_games(): | ||||||
|     games = Game.query.filter_by(user_id=current_user.id).all() |     games = Game.query.filter_by(user_id=current_user.id).all() | ||||||
|     output = io.StringIO() |     output = io.StringIO() | ||||||
|     writer = csv.writer(output) |     writer = csv.writer(output) | ||||||
|  |      | ||||||
|     writer.writerow(['Name', 'Steam Key', 'Status', 'Recipient', 'Notes', 'URL', 'Created', 'Redeem by', 'Steam AppID']) |     writer.writerow(['Name', 'Steam Key', 'Status', 'Recipient', 'Notes', 'URL', 'Created', 'Redeem by', 'Steam AppID']) | ||||||
|  |      | ||||||
|     for game in games: |     for game in games: | ||||||
|         writer.writerow([ |         writer.writerow([ | ||||||
|             game.name, game.steam_key, game.status, game.recipient, game.notes, |             game.name,  | ||||||
|             game.url, game.created_at.strftime('%Y-%m-%d %H:%M:%S') if game.created_at else '', |             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.redeem_date.strftime('%Y-%m-%d') if game.redeem_date else '', | ||||||
|             game.steam_appid |             game.steam_appid | ||||||
|         ]) |         ]) | ||||||
|  |      | ||||||
|     output.seek(0) |     output.seek(0) | ||||||
|     return send_file( |     return send_file( | ||||||
|         io.BytesIO(output.getvalue().encode('utf-8')), |         io.BytesIO(output.getvalue().encode('utf-8')), | ||||||
|  | @ -222,35 +373,306 @@ def export_games(): | ||||||
|         download_name='games_export.csv' |         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']) | @app.route('/import', methods=['GET', 'POST']) | ||||||
| @login_required | @login_required | ||||||
| def import_games(): | def import_games(): | ||||||
|     if request.method == 'POST': |     if request.method == 'POST': | ||||||
|         file = request.files.get('file') |         file = request.files.get('file') | ||||||
|  |          | ||||||
|         if file and file.filename.endswith('.csv'): |         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) |             reader = csv.DictReader(stream) | ||||||
|             for row in reader: |             new_games = 0 | ||||||
|                 new_game = Game( |             duplicates = 0 | ||||||
|                     name=row['Name'], |              | ||||||
|                     steam_key=row['Steam Key'], |             try: | ||||||
|                     status=row['Status'], |                 with db.session.begin_nested(): | ||||||
|                     recipient=row.get('Recipient', ''), |                     for row in reader: | ||||||
|                     notes=row.get('Notes', ''), |                         steam_key = row['Steam Key'].strip() | ||||||
|                     url=row.get('URL', ''), |                          | ||||||
|                     created_at=datetime.strptime(row['Created'], '%Y-%m-%d %H:%M:%S') if row.get('Created') else datetime.utcnow(), |                         if Game.query.filter_by(steam_key=steam_key).first(): | ||||||
|                     redeem_date=datetime.strptime(row['Redeem by'], '%Y-%m-%d') if row.get('Redeem by') else None, |                             duplicates += 1 | ||||||
|                     steam_appid=row.get('Steam AppID', ''), |                             continue | ||||||
|                     user_id=current_user.id |                          | ||||||
|                 ) |                         game = Game( | ||||||
|                 db.session.add(new_game) |                             name=row['Name'], | ||||||
|             db.session.commit() |                             steam_key=steam_key, | ||||||
|             flash(_('Import erfolgreich!'), 'success') |                             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')) |             return redirect(url_for('index')) | ||||||
|  |          | ||||||
|         flash(_('Bitte eine gültige CSV-Datei hochladen.'), 'danger') |         flash(_('Bitte eine gültige CSV-Datei hochladen.'), 'danger') | ||||||
|  |      | ||||||
|     return render_template('import.html') |     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__': | if __name__ == '__main__': | ||||||
|     with app.app_context(): |     with app.app_context(): | ||||||
|         db.create_all() |         db.create_all() | ||||||
|     app.run(host='0.0.0.0', port=5000) |     app.run(host='0.0.0.0', port=5000) | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -1,2 +1,3 @@ | ||||||
| [python: **.py] | [python: **.py] | ||||||
| [jinja2: templates/**.html] | [jinja2: **/templates/**.html] | ||||||
|  | extensions=jinja2.ext.autoescape,jinja2.ext.with_ | ||||||
|  |  | ||||||
|  | @ -1,13 +1,14 @@ | ||||||
| version: '3.8' |  | ||||||
| 
 |  | ||||||
| services: | services: | ||||||
|   steam-manager: |   steam-manager: | ||||||
|     build: . |     build: . | ||||||
|     ports: |     ports: | ||||||
|       - "5000:5000" |       - "5000:5000" | ||||||
|     volumes: |  | ||||||
|       - /root/test/data:/app/data |  | ||||||
|       - /root/test/steam-translations:/app/translations |  | ||||||
|     environment: |     environment: | ||||||
|       - FLASK_DEBUG=0 |       - REGISTRATION_ENABLED=True | ||||||
|  |       - TZ= | ||||||
|  |     volumes: | ||||||
|  |       - ../data:/app/data | ||||||
|  |       - ../translations:/app/translations | ||||||
|  |       - ../.env:/app/.env | ||||||
|  |     user: "1000:1000" | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|  |  | ||||||
|  | @ -1,7 +1,16 @@ | ||||||
| flask | flask | ||||||
| flask-login | flask-login | ||||||
|  | flask-wtf | ||||||
|  | flask-migrate | ||||||
| werkzeug | werkzeug | ||||||
| python-dotenv | python-dotenv | ||||||
| flask-sqlalchemy | flask-sqlalchemy | ||||||
| flask-babel | flask-babel | ||||||
| jinja2<3.1.0 | jinja2<3.1.0 | ||||||
|  | itsdangerous | ||||||
|  | sqlalchemy | ||||||
|  | apscheduler | ||||||
|  | matrix-client | ||||||
|  | reportlab | ||||||
|  | requests | ||||||
|  | pillow | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								steam-gift-manager/static/forgejo.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								steam-gift-manager/static/forgejo.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 212 212" width="32" height="32"><style>circle,path{fill:none;stroke:#000;stroke-width:15}path{stroke-width:25}.orange{stroke:#f60}.red{stroke:#d40000}</style><g transform="translate(6 6)"><path d="M58 168V70a50 50 0 0 1 50-50h20" class="orange"/><path d="M58 168v-30a50 50 0 0 1 50-50h20" class="red"/><circle cx="142" cy="20" r="18" class="orange"/><circle cx="142" cy="88" r="18" class="red"/><circle cx="58" cy="180" r="18" class="red"/></g></svg> | ||||||
| After Width: | Height: | Size: 503 B | 
							
								
								
									
										
											BIN
										
									
								
								steam-gift-manager/static/logo.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								steam-gift-manager/static/logo.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 52 KiB | 
							
								
								
									
										
											BIN
										
									
								
								steam-gift-manager/static/logo_small.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								steam-gift-manager/static/logo_small.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 35 KiB | 
|  | @ -31,3 +31,32 @@ body { | ||||||
|     font-size: 0.9em; |     font-size: 0.9em; | ||||||
|     font-weight: 500; |     font-weight: 500; | ||||||
| } | } | ||||||
|  | #expiry-countdown { | ||||||
|  |   font-weight: 600; | ||||||
|  |   letter-spacing: 0.05em; | ||||||
|  |   color: #dc3545; | ||||||
|  |   transition: color 0.3s ease; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | [data-bs-theme="dark"] #expiry-countdown { | ||||||
|  |   color: #ff6b6b; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* Progressbar-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"> | <div class="card p-4 shadow-sm"> | ||||||
|     <h2 class="mb-4">{{ _('Add New Game') }}</h2> |     <h2 class="mb-4">{{ _('Add New Game') }}</h2> | ||||||
|     <form method="POST"> |     <form method="POST"> | ||||||
|  | 	<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||||
|         <div class="row g-3"> |         <div class="row g-3"> | ||||||
|             <div class="col-md-6"> |             <div class="col-md-6"> | ||||||
|                 <label class="form-label">{{ _('Name') }} *</label> |                 <label class="form-label">{{ _('Name') }} *</label> | ||||||
|                 <input type="text" name="name" class="form-control" required> |                 <input type="text" name="name" class="form-control" required> | ||||||
|             </div> |             </div> | ||||||
|             <div class="col-md-6"> |             <div class="col-md-6"> | ||||||
|                 <label class="form-label">{{ _('Steam Key') }} *</label> |                 <label class="form-label">{{ _('Game Key') }} *</label> | ||||||
|                 <input type="text" name="steam_key" class="form-control" required> |                 <input type="text" name="steam_key" class="form-control" required> | ||||||
|             </div> |             </div> | ||||||
|             <div class="col-md-4"> |             <div class="col-md-4"> | ||||||
|  |  | ||||||
|  | @ -3,14 +3,18 @@ | ||||||
| <head> | <head> | ||||||
|     <meta charset="UTF-8"> |     <meta charset="UTF-8"> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> |     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|     <title>{{ _('Steam Manager') }}</title> |     <meta name="csrf-token" content="{{ csrf_token() }}"> | ||||||
|  |     <title>{{ _('Game Key Manager') }}</title> | ||||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> |     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> | ||||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> |     <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|     <nav class="navbar navbar-expand-lg bg-body-tertiary"> |     <nav class="navbar navbar-expand-lg bg-body-tertiary"> | ||||||
|         <div class="container"> |         <div class="container"> | ||||||
|             <a class="navbar-brand" href="/">{{ _('Steam Manager') }}</a> |             <a class="navbar-brand d-flex align-items-center gap-2" href="/"> | ||||||
|  |                  <img src="{{ url_for('static', filename='logo_small.png') }}" alt="Logo" width="150" height="116" style="object-fit:contain; border-radius:8px;"> | ||||||
|  |                  <span>Game Key Manager</span> | ||||||
|  |             </a> | ||||||
|             <div class="d-flex align-items-center gap-3"> |             <div class="d-flex align-items-center gap-3"> | ||||||
|                 <form class="d-flex" action="{{ url_for('index') }}" method="GET"> |                 <form class="d-flex" action="{{ url_for('index') }}" method="GET"> | ||||||
|                     <input class="form-control me-2"  |                     <input class="form-control me-2"  | ||||||
|  | @ -26,28 +30,22 @@ | ||||||
|                            id="darkModeSwitch" {% if theme == 'dark' %}checked{% endif %}> |                            id="darkModeSwitch" {% if theme == 'dark' %}checked{% endif %}> | ||||||
|                     <label class="form-check-label" for="darkModeSwitch">{{ _('Dark Mode') }}</label> |                     <label class="form-check-label" for="darkModeSwitch">{{ _('Dark Mode') }}</label> | ||||||
|                 </div> |                 </div> | ||||||
|                 <!-- Sprachumschalter --> |  | ||||||
|                 <div class="dropdown ms-3"> |                 <div class="dropdown ms-3"> | ||||||
|                   <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> |                     <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> | ||||||
|                     {% if get_locale() == 'de' %} Deutsch {% elif get_locale() == 'en' %} English {% else %} Sprache {% endif %} |                         {% if get_locale() == 'de' %} Deutsch {% elif get_locale() == 'en' %} English {% else %} Sprache {% endif %} | ||||||
|                   </button> |                     </button> | ||||||
|                   <ul class="dropdown-menu"> |                     <ul class="dropdown-menu"> | ||||||
|                     <li> |                         <li><a class="dropdown-item {% if get_locale() == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li> | ||||||
|                       <a class="dropdown-item {% if get_locale() == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}"> |                         <li><a class="dropdown-item {% if get_locale() == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li> | ||||||
|                         Deutsch |                     </ul> | ||||||
|                       </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> |                 </div> | ||||||
|                 {% if current_user.is_authenticated %} |                 {% if current_user.is_authenticated %} | ||||||
| 					<a href="{{ url_for('export_games') }}" class="btn btn-outline-secondary">⬇️ {{ _('Export') }}</a> |                         <li class="nav-item"> | ||||||
| 					<a href="{{ url_for('import_games') }}" class="btn btn-outline-secondary">⬆️ {{ _('Import') }}</a>				 |                              <a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Passwort') }}</a> | ||||||
|                     <a href="{{ url_for('logout') }}" class="btn btn-danger ms-3">{{ _('Logout') }}</a> |                         </li> | ||||||
|  |                         <li class="nav-item"> | ||||||
|  |                              <a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a> | ||||||
|  |                         </li> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|             </div> |             </div> | ||||||
|         </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 src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> | ||||||
|     <script> |     <script> | ||||||
|     document.addEventListener('DOMContentLoaded', function() { |     document.addEventListener('DOMContentLoaded', function() { | ||||||
|         const toggle = document.getElementById('darkModeSwitch'); |         const toggle = document.getElementById('darkModeSwitch') | ||||||
|         const html = document.documentElement; |         const html = document.documentElement | ||||||
|  |          | ||||||
|         toggle.addEventListener('change', function() { |         toggle.addEventListener('change', function() { | ||||||
|             const theme = this.checked ? 'dark' : 'light'; |             const theme = this.checked ? 'dark' : 'light' | ||||||
|             fetch('/set-theme/' + theme) |             fetch('/set-theme/' + theme) | ||||||
|                 .then(() => { |                 .then(() => html.setAttribute('data-bs-theme', theme)) | ||||||
|                     html.setAttribute('data-bs-theme', theme); |         }) | ||||||
|                 }); |     }) | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|     </script> |     </script> | ||||||
|  | {% include "footer.html" %} | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
							
								
								
									
										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"> | <div class="card p-4 shadow-sm"> | ||||||
|     <h2 class="mb-4">{{ _('Edit Game') }}</h2> |     <h2 class="mb-4">{{ _('Edit Game') }}</h2> | ||||||
|     <form method="POST"> |     <form method="POST"> | ||||||
|  |         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||||
|         <div class="row g-3"> |         <div class="row g-3"> | ||||||
|             <div class="col-md-6"> |             <div class="col-md-6"> | ||||||
|                 <label class="form-label">{{ _('Name') }} *</label> |                 <label class="form-label">{{ _('Name') }} *</label> | ||||||
|                 <input type="text" name="name" class="form-control" value="{{ game.name }}" required> |                 <input type="text" name="name" class="form-control" value="{{ game.name }}" required> | ||||||
|             </div> |             </div> | ||||||
|             <div class="col-md-6"> |             <div class="col-md-6"> | ||||||
|                 <label class="form-label">{{ _('Steam Key') }} *</label> |                 <label class="form-label">{{ _('Game Key') }} *</label> | ||||||
|                 <input type="text" name="steam_key" class="form-control" value="{{ game.steam_key }}" required> |                 <input type="text" name="steam_key" class="form-control" value="{{ game.steam_key }}" required> | ||||||
|             </div> |             </div> | ||||||
| 			<div class="col-md-6"> |             <div class="col-md-6"> | ||||||
| 			    <label class="form-label">{{ _('Steam AppID (optional)') }}</label> |                 <label class="form-label">{{ _('Steam AppID (optional)') }}</label> | ||||||
|                 <input type="text" name="steam_appid" class="form-control" value="{{ game.steam_appid or '' }}"> |                 <input type="text" name="steam_appid" class="form-control" value="{{ game.steam_appid or '' }}"> | ||||||
| 			</div> |             </div> | ||||||
|             <div class="col-md-4"> |             <div class="col-md-4"> | ||||||
|                 <label class="form-label">{{ _('Status') }} *</label> |                 <label class="form-label">{{ _('Status') }} *</label> | ||||||
|                 <select name="status" class="form-select" required> |                 <select name="status" class="form-select" required> | ||||||
|  | @ -40,6 +41,21 @@ | ||||||
|                 <label class="form-label">{{ _('Notes') }}</label> |                 <label class="form-label">{{ _('Notes') }}</label> | ||||||
|                 <textarea name="notes" class="form-control" rows="3">{{ game.notes }}</textarea> |                 <textarea name="notes" class="form-control" rows="3">{{ game.notes }}</textarea> | ||||||
|             </div> |             </div> | ||||||
|  |             <div class="col-12"> | ||||||
|  |                 {% if redeem_url and active_redeem %} | ||||||
|  |                 <div class="mb-3"> | ||||||
|  |                     <label class="form-label">{{ _('Active Redeem Link') }}</label> | ||||||
|  |                     <input type="text" | ||||||
|  |                            class="form-control" | ||||||
|  |                            value="{{ redeem_url }}" | ||||||
|  |                            readonly | ||||||
|  |                            onclick="this.select()"> | ||||||
|  |                     <small class="text-muted"> | ||||||
|  |                         {{ _('Expires at') }}: {{ active_redeem.expires.strftime('%d.%m.%Y %H:%M') }} | ||||||
|  |                     </small> | ||||||
|  |                 </div> | ||||||
|  |                 {% endif %} | ||||||
|  |             </div> | ||||||
|             <div class="col-12"> |             <div class="col-12"> | ||||||
|                 <button type="submit" class="btn btn-primary">{{ _('Save') }}</button> |                 <button type="submit" class="btn btn-primary">{{ _('Save') }}</button> | ||||||
|                 <a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a> |                 <a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a> | ||||||
|  |  | ||||||
							
								
								
									
										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"> | <div class="card p-4 shadow-sm"> | ||||||
|     <h2 class="mb-4">{{ _('Import Games') }}</h2> |     <h2 class="mb-4">{{ _('Import Games') }}</h2> | ||||||
|     <form method="POST" enctype="multipart/form-data"> |     <form method="POST" enctype="multipart/form-data"> | ||||||
|  | 	<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||||
|         <div class="mb-3"> |         <div class="mb-3"> | ||||||
|             <label class="form-label">{{ _('CSV-Datei auswählen') }}</label> |             <label class="form-label">{{ _('CSV-Datei auswählen') }}</label> | ||||||
|             <input type="file" name="file" class="form-control" accept=".csv" required> |             <input type="file" name="file" class="form-control" accept=".csv" required> | ||||||
|  |  | ||||||
|  | @ -2,9 +2,12 @@ | ||||||
| {% block content %} | {% block content %} | ||||||
| <div class="d-flex justify-content-between align-items-center mb-4"> | <div class="d-flex justify-content-between align-items-center mb-4"> | ||||||
|     <h1>{{ _('My Games') }}</h1> |     <h1>{{ _('My Games') }}</h1> | ||||||
|     <a href="{{ url_for('add_game') }}" class="btn btn-primary"> |     <div> | ||||||
|         + {{ _('Add New Game') }} |         <a href="{{ url_for('export_games') }}" class="btn btn-outline-secondary">⬇️ {{ _('Export CSV') }}</a> | ||||||
|     </a> |         <a href="{{ url_for('export_pdf') }}" class="btn btn-outline-secondary">⬇️ Export PDF (for sharing)</a> | ||||||
|  |         <a href="{{ url_for('import_games') }}" class="btn btn-outline-secondary">⬆️ {{ _('Import CSV') }}</a> | ||||||
|  |         <a href="{{ url_for('add_game') }}" class="btn btn-primary">+ {{ _('Add New Game') }}</a> | ||||||
|  |     </div> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| {% if games %} | {% if games %} | ||||||
|  | @ -54,8 +57,16 @@ | ||||||
|                     {% endif %} |                     {% endif %} | ||||||
|                 </td> |                 </td> | ||||||
|                 <td class="text-nowrap"> |                 <td class="text-nowrap"> | ||||||
|  |                     {% if game.status == 'verschenkt' %} | ||||||
|  |                     <button class="btn btn-sm btn-success generate-redeem"  | ||||||
|  |                             data-game-id="{{ game.id }}" | ||||||
|  |                             title="{{ _('Generate redeem link') }}"> | ||||||
|  |                         🔗 | ||||||
|  |                     </button> | ||||||
|  |                     {% endif %} | ||||||
|                     <a href="{{ url_for('edit_game', game_id=game.id) }}" class="btn btn-sm btn-warning">✏️</a> |                     <a href="{{ url_for('edit_game', game_id=game.id) }}" class="btn btn-sm btn-warning">✏️</a> | ||||||
|                     <form method="POST" action="{{ url_for('delete_game', game_id=game.id) }}" class="d-inline"> |                     <form method="POST" action="{{ url_for('delete_game', game_id=game.id) }}" class="d-inline"> | ||||||
|  |                         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||||
|                         <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ _('Really delete?') }}')">🗑️</button> |                         <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ _('Really delete?') }}')">🗑️</button> | ||||||
|                     </form> |                     </form> | ||||||
|                 </td> |                 </td> | ||||||
|  | @ -64,6 +75,33 @@ | ||||||
|         </tbody> |         </tbody> | ||||||
|     </table> |     </table> | ||||||
| </div> | </div> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); | ||||||
|  | 
 | ||||||
|  | document.querySelectorAll('.generate-redeem').forEach(btn => { | ||||||
|  |     btn.addEventListener('click', async function() { | ||||||
|  |         const gameId = this.dataset.gameId; | ||||||
|  |         try { | ||||||
|  |             const response = await fetch('/generate_redeem/' + gameId, { | ||||||
|  |                 method: 'POST', | ||||||
|  |                 headers: { | ||||||
|  |                     'X-CSRFToken': csrfToken | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |             if (!response.ok) throw new Error('Network error'); | ||||||
|  |             const data = await response.json(); | ||||||
|  |             if(data.url) { | ||||||
|  |                 await navigator.clipboard.writeText(data.url); | ||||||
|  |                 alert('{{ _("Redeem link copied to clipboard!") }}'); | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('Error:', error); | ||||||
|  |             alert('{{ _("Error generating link") }}'); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| {% else %} | {% else %} | ||||||
| <div class="alert alert-info">{{ _('No games yet') }}</div> | <div class="alert alert-info">{{ _('No games yet') }}</div> | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|  | @ -3,9 +3,11 @@ | ||||||
| <div class="row justify-content-center mt-5"> | <div class="row justify-content-center mt-5"> | ||||||
|     <div class="col-md-6"> |     <div class="col-md-6"> | ||||||
|         <div class="card shadow-sm"> |         <div class="card shadow-sm"> | ||||||
|             <div class="card-body"> |             <div class="card-body text-center"> | ||||||
|  |                 <img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" width="311" height="240" class="mb-4" style="object-fit:contain;"> | ||||||
|                 <h2 class="card-title mb-4">{{ _('Login') }}</h2> |                 <h2 class="card-title mb-4">{{ _('Login') }}</h2> | ||||||
|                 <form method="POST"> |                 <form method="POST"> | ||||||
|  |                     <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||||
|                     <div class="mb-3"> |                     <div class="mb-3"> | ||||||
|                         <label class="form-label">{{ _('Username') }}</label> |                         <label class="form-label">{{ _('Username') }}</label> | ||||||
|                         <input type="text" name="username" class="form-control" required> |                         <input type="text" name="username" class="form-control" required> | ||||||
|  | @ -24,3 +26,4 @@ | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  | 
 | ||||||
|  |  | ||||||
							
								
								
									
										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"> |             <div class="card-body"> | ||||||
|                 <h2 class="card-title mb-4">{{ _('Register') }}</h2> |                 <h2 class="card-title mb-4">{{ _('Register') }}</h2> | ||||||
|                 <form method="POST"> |                 <form method="POST"> | ||||||
|  |                     <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> | ||||||
|                     <div class="mb-3"> |                     <div class="mb-3"> | ||||||
|                         <label class="form-label">{{ _('Username') }}</label> |                         <label class="form-label">{{ _('Username') }}</label> | ||||||
|                         <input type="text" name="username" class="form-control" required> |                         <input type="text" name="username" class="form-control" required> | ||||||
|  |  | ||||||
|  | @ -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" | 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 . | docker-compose exec steam-manager pybabel extract -F babel.cfg -o translations/messages.pot . | ||||||
| 
 | 
 | ||||||
| # 2. Initialisiere Sprachen (nur einmal nötig, danach auskommentieren) | # Für jede Sprache prüfen und ggf. initialisieren | ||||||
| for lang in de en; do | for lang in "${!locales[@]}"; do | ||||||
|     if [ ! -f "../steam-translations/$lang/LC_MESSAGES/messages.po" ]; then |   if [ ! -f "translations/${locales[$lang]}/LC_MESSAGES/messages.po" ]; then | ||||||
|         docker-compose exec steam-manager pybabel init -i translations/messages.pot -d translations -l $lang |     docker-compose exec steam-manager pybabel init \ | ||||||
|     fi |       -i translations/messages.pot \ | ||||||
|  |       -d translations \ | ||||||
|  |       -l "${locales[$lang]}" | ||||||
|  |   fi | ||||||
| done | done | ||||||
| 
 | 
 | ||||||
| # 3. Aktualisiere Übersetzungen | # Übersetzungen aktualisieren und kompilieren | ||||||
| docker-compose exec steam-manager pybabel update -i translations/messages.pot -d translations | docker-compose exec steam-manager pybabel update -i translations/messages.pot -d translations | ||||||
| 
 |  | ||||||
| # 4. Kompiliere Übersetzungen |  | ||||||
| docker-compose exec steam-manager pybabel compile -d translations | docker-compose exec steam-manager pybabel compile -d translations | ||||||
| 
 | 
 | ||||||
| echo "✅ Übersetzungen extrahiert, aktualisiert und kompiliert!" | echo "✅ Ü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