Compare commits
No commits in common. "7bf20396e640df194be147a67d888b39461a2b7d" and "d9bca6f24c823012d47f495c2b516f4f95120a62" have entirely different histories.
7bf20396e6
...
d9bca6f24c
30 changed files with 994 additions and 2785 deletions
BIN
GameManager.png
BIN
GameManager.png
Binary file not shown.
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 48 KiB |
97
README.md
97
README.md
|
@ -1,50 +1,34 @@
|
|||
# 🔑 Game Key Manager 🔑
|
||||
# 🔑 Game Key Management System 🔑
|
||||
|
||||
## 👋 Welcome! 👋
|
||||

|
||||
|
||||
## Welcome! 👋
|
||||
|
||||
This project helps you keep track of your collected game keys.
|
||||
No more confusion about whether a key is redeemed, gifted, or still unused – now you have everything in one place, with search, status, and even automatic Steam cover images!
|
||||
|
||||
It's even possible to gift your keys via a unique website. Just edit the game to "Gifted" and you'll get a option to copy the on your overview page. (maybe HTTPS only)
|
||||
|
||||
(the link will also remain in the edit area)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ✨ Features ✨
|
||||
|
||||
- **Key Management:**
|
||||
Enter your game keys, the corresponding game, platform, and maybe where you got the key.
|
||||
Enter your game keys, the corresponding game, platform, and where you got the key.
|
||||
- **Status Tracking:**
|
||||
Mark keys as "Redeemed", "Gifted" or "Available" – always know your status.
|
||||
- **Shop URL & Steam Cover:**
|
||||
Save the shop URL and (optionally) the Steam AppID. The app will automatically show the official Steam cover image if available.
|
||||
- **Gift your Games:**
|
||||
You can create a unique redeem/gift website, which will expire after 24h.
|
||||
- **Multi-user:**
|
||||
Each user manages their own keys.
|
||||
- **Enable/Disable Registrations:**
|
||||
Perfect if you want to run the Server just on your own (via .env file)
|
||||
- **Search:**
|
||||
- **Search & Filter:**
|
||||
Find games quickly with the search function.
|
||||
- **Responsive UI:**
|
||||
Works on desktop and mobile, with Dark Mode toggle.
|
||||
- **Multi-language:**
|
||||
Switch between English and German instantly*.
|
||||
- **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
|
||||
Switch between English and German instantly.
|
||||
- **No key data leaves your server!**
|
||||
- **(Planned):**
|
||||
- ~~Import/Export (CSV)~~
|
||||
- ~~Redeem site with unique sharing link~~
|
||||
- Import/Export (CSV, JSON)
|
||||
- Redeem site with unique sharing link
|
||||
|
||||
---
|
||||
|
||||
|
@ -53,14 +37,13 @@ It's even possible to gift your keys via a unique website. Just edit the game to
|
|||
### 1. **Clone the Repository**
|
||||
|
||||
```bash
|
||||
git clone https://git.nocci.it/nocci/GameKeyManager
|
||||
git clone https://git.nocci.it/nocci/GiftGamesDB
|
||||
cd steam-gift-manager
|
||||
```
|
||||
|
||||
### 2. **Setup Docker**
|
||||
|
||||
Make sure you have [Docker](https://www.docker.com/) and [docker-compose](https://docs.docker.com/compose/) installed.
|
||||
If not, the script will ask you what to do and can install Docker and docker-compose for you. (maybe not if you are running Arch)
|
||||
|
||||
### 3. **Initial Setup**
|
||||
|
||||
|
@ -79,90 +62,54 @@ docker-compose build --no-cache
|
|||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 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)**
|
||||
### 5. **Initialize and Edit Translations (Optional)**
|
||||
|
||||
```bash
|
||||
./translate.sh
|
||||
```
|
||||
|
||||
Edit the .po files in translations/de_DE/LC_MESSAGES/messages.po and en_US/LC_MESSAGES/messages.po
|
||||
Edit the .po files in steam-translations/de/LC_MESSAGES/messages.po and en/LC_MESSAGES/messages.po
|
||||
|
||||
```bash
|
||||
./translate.sh
|
||||
cd steam-gift-manager/
|
||||
docker-compose down && docker-compose up -d --build
|
||||
docker-compose restart steam-manager
|
||||
```
|
||||
|
||||
### 7. **Open the App**
|
||||
### 6. **Open the App**
|
||||
|
||||
Go to [http://localhost:5000](http://localhost:5000) in your browser.
|
||||
|
||||
- Register your first user.
|
||||
- Add your keys, shop URLs etc.
|
||||
- Add your keys, shop URLs, and (optionally) Steam AppIDs.
|
||||
- Enjoy search, status, and automatic Steam cover images!
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technology Stack 🛠️
|
||||
|
||||
- **Frontend:** Bootstrap 5, Jinja2 Templates ...
|
||||
- **Backend:** Python 3, Flask, Flask-Babel, Flask-Login, Flask-SQLAlchemy ...
|
||||
- **Frontend:** Bootstrap 5, Jinja2 Templates
|
||||
- **Backend:** Python 3, Flask, Flask-Babel, Flask-Login, Flask-SQLAlchemy
|
||||
- **Database:** SQLite (persisted in `data/`)
|
||||
- **Containerization:** Docker, docker-compose
|
||||
- **Translations:** Flask-Babel, editable `.po` files in `translations/`
|
||||
- **Translations:** Flask-Babel, editable `.po` files in `steam-translations/`
|
||||
|
||||
## 🌍 Multi-language
|
||||
|
||||
- Switch between English and German using the dropdown in the navigation bar.
|
||||
- All game and menu texts can be translated or individualized.
|
||||
|
||||
## 🔔 Notifications
|
||||
|
||||
- Send notifications if a game has to be redeemed by a specific date
|
||||
- Gotify, Matrix and Pushover are already available - have a look into the .env file
|
||||
- 48 hours before you are running out of time the app will send you a notice
|
||||
|
||||
---
|
||||
|
||||
## 🪙 Do you this project? 🪙
|
||||
|
||||
If you’d like to support itme, you can make a donation here:
|
||||
|
||||
[](https://ko-fi.com/nocci)
|
||||
|
||||
[](https://liberapay.com/nocci/donate)
|
||||
|
||||
Thank you!
|
||||
- All game and menu texts are translated.
|
||||
- You can add more languages by editing the `.po` files and running `./translate.sh`.
|
||||
|
||||
---
|
||||
|
||||
## 🙌 Contribute! 🙌
|
||||
|
||||
This project is open source!
|
||||
This project is open source and thrives on your help!
|
||||
|
||||
- **Bug Reports:** Please report bugs as Issues.
|
||||
- **Feature Requests:** Suggest new features!
|
||||
- **Pull Requests:** Submit your code changes!
|
||||
|
||||
// **only possible after Forgejo opens for federation** \\\
|
||||
|
||||
---
|
||||
|
||||
## 📜 License 📜
|
||||
|
@ -177,4 +124,4 @@ A big thank you to everyone who supports and contributes to this project!
|
|||
|
||||
---
|
||||
|
||||
**Enjoy your organized Game key collection!** 🚀
|
||||
**Enjoy your organized Steam key collection!** 🚀
|
||||
|
|
|
@ -2,13 +2,7 @@ FROM python:3.10-slim
|
|||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
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
|
||||
|
||||
RUN mkdir -p /app/data && chmod -R a+rwX /app/data
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
|
|
|
@ -1,90 +1,26 @@
|
|||
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 import Flask, render_template, request, redirect, url_for, flash, make_response, session, abort, send_file
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
||||
from flask_babel import Babel, _
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime, timedelta
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask import abort
|
||||
import io
|
||||
import warnings
|
||||
import re
|
||||
from datetime import datetime
|
||||
import os
|
||||
import io
|
||||
import csv
|
||||
import secrets
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(override=True)
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import atexit
|
||||
from flask_migrate import Migrate
|
||||
from sqlalchemy import MetaData
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import A4, landscape, letter
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate,
|
||||
Table,
|
||||
TableStyle,
|
||||
Paragraph,
|
||||
Image,
|
||||
Spacer
|
||||
)
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.lib.units import cm, inch, mm
|
||||
from io import BytesIO
|
||||
import reportlab.lib
|
||||
|
||||
app = Flask(__name__)
|
||||
csrf = CSRFProtect(app)
|
||||
app.config['SECRET_KEY'] = os.urandom(24)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/data/games.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['BABEL_DEFAULT_LOCALE'] = 'de'
|
||||
app.config['BABEL_SUPPORTED_LOCALES'] = ['de', 'en']
|
||||
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
|
||||
|
||||
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)
|
||||
db = SQLAlchemy(app)
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = 'login'
|
||||
babel = Babel(app)
|
||||
|
||||
# Logging
|
||||
app.logger.addHandler(logging.StreamHandler())
|
||||
app.logger.setLevel(logging.INFO)
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']:
|
||||
|
@ -98,63 +34,36 @@ def inject_template_vars():
|
|||
theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light'
|
||||
)
|
||||
|
||||
# Datenbankmodelle
|
||||
class User(db.Model, UserMixin):
|
||||
__tablename__ = 'users'
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
password = db.Column(db.String(256), nullable=False)
|
||||
games = db.relationship('Game', back_populates='owner', lazy=True)
|
||||
username = db.Column(db.String(100), unique=True)
|
||||
password = db.Column(db.String(100))
|
||||
games = db.relationship('Game', backref='owner', lazy=True)
|
||||
|
||||
class Game(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
owner = db.relationship('User', back_populates='games')
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
steam_key = db.Column(db.String(100), nullable=False, unique=True)
|
||||
steam_key = db.Column(db.String(100), nullable=False)
|
||||
status = db.Column(db.String(50), nullable=False)
|
||||
recipient = db.Column(db.String(100))
|
||||
notes = db.Column(db.Text)
|
||||
url = db.Column(db.String(200))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
redeem_date = db.Column(db.DateTime)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
steam_appid = db.Column(db.String(20))
|
||||
|
||||
class RedeemToken(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
token = db.Column(db.String(17), unique=True, nullable=False)
|
||||
game_id = db.Column(db.Integer, db.ForeignKey('game.id'), nullable=False)
|
||||
expires = db.Column(db.DateTime, nullable=False)
|
||||
used = db.Column(db.Boolean, default=False)
|
||||
total_hours = db.Column(db.Integer, nullable=False)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return db.session.get(User, int(user_id))
|
||||
|
||||
def extract_steam_appid(url):
|
||||
match = re.search(r'store\.steampowered\.com/app/(\d+)', url or '')
|
||||
return match.group(1) if match else ''
|
||||
|
||||
# 404
|
||||
def get_or_404(model, id):
|
||||
instance = db.session.get(model, id)
|
||||
if not instance:
|
||||
abort(404)
|
||||
return instance
|
||||
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
search_query = request.args.get('q', '')
|
||||
query = Game.query.filter_by(user_id=current_user.id)
|
||||
|
||||
query = db.session.query(Game).filter_by(user_id=current_user.id)
|
||||
if search_query:
|
||||
query = query.filter(Game.name.ilike(f'%{search_query}%'))
|
||||
|
||||
games = query.order_by(Game.created_at.desc()).all()
|
||||
return render_template('index.html',
|
||||
games=games,
|
||||
|
@ -179,34 +88,25 @@ def login():
|
|||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user and check_password_hash(user.password, password):
|
||||
login_user(user)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
flash(_('Invalid credentials'), 'danger')
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if not app.config['REGISTRATION_ENABLED']:
|
||||
flash(_('Registrierungen sind deaktiviert'), 'danger')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = generate_password_hash(request.form['password'])
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash(_('Username already exists'), 'danger')
|
||||
return redirect(url_for('register'))
|
||||
|
||||
new_user = User(username=username, password=password)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
login_user(new_user)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render_template('register.html')
|
||||
|
||||
@app.route('/logout')
|
||||
|
@ -215,28 +115,13 @@ def logout():
|
|||
logout_user()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@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']
|
||||
import re
|
||||
|
||||
if not check_password_hash(current_user.password, current_password):
|
||||
flash(_('Aktuelles Passwort ist falsch'), 'danger')
|
||||
return redirect(url_for('change_password'))
|
||||
|
||||
if new_password != confirm_password:
|
||||
flash(_('Neue Passwörter stimmen nicht überein'), 'danger')
|
||||
return redirect(url_for('change_password'))
|
||||
|
||||
current_user.password = generate_password_hash(new_password)
|
||||
db.session.commit()
|
||||
flash(_('Passwort erfolgreich geändert'), 'success')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render_template('change_password.html')
|
||||
def extract_steam_appid(url):
|
||||
match = re.search(r'store\.steampowered\.com/app/(\d+)', url or '')
|
||||
if match:
|
||||
return match.group(1)
|
||||
return ''
|
||||
|
||||
@app.route('/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
|
@ -245,10 +130,8 @@ def add_game():
|
|||
try:
|
||||
url = request.form.get('url', '')
|
||||
steam_appid = request.form.get('steam_appid', '').strip()
|
||||
|
||||
if not steam_appid:
|
||||
steam_appid = extract_steam_appid(url)
|
||||
|
||||
new_game = Game(
|
||||
name=request.form['name'],
|
||||
steam_key=request.form['steam_key'],
|
||||
|
@ -260,19 +143,13 @@ def add_game():
|
|||
redeem_date=datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
db.session.add(new_game)
|
||||
db.session.commit()
|
||||
flash(_('Game added successfully!'), 'success')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
flash(_('Steam Key already exists!'), 'danger')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(_('Error: ') + str(e), 'danger')
|
||||
|
||||
return render_template('add_game.html')
|
||||
|
||||
@app.route('/edit/<int:game_id>', methods=['GET', 'POST'])
|
||||
|
@ -280,26 +157,14 @@ def add_game():
|
|||
def edit_game(game_id):
|
||||
game = db.session.get(Game, game_id)
|
||||
if not game or game.owner != current_user:
|
||||
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
|
||||
return _("Not allowed!"), 403
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
url = request.form.get('url', '')
|
||||
steam_appid = request.form.get('steam_appid', '').strip()
|
||||
|
||||
if not steam_appid:
|
||||
steam_appid = extract_steam_appid(url)
|
||||
|
||||
game.name = request.form['name']
|
||||
game.steam_key = request.form['steam_key']
|
||||
game.status = request.form['status']
|
||||
|
@ -308,40 +173,32 @@ def edit_game(game_id):
|
|||
game.url = url
|
||||
game.steam_appid = steam_appid
|
||||
game.redeem_date = datetime.strptime(request.form['redeem_date'], '%Y-%m-%d') if request.form['redeem_date'] else None
|
||||
|
||||
db.session.commit()
|
||||
flash(_('Changes saved!'), 'success')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(_('Error: ') + str(e), 'danger')
|
||||
|
||||
return render_template('edit_game.html',
|
||||
game=game,
|
||||
redeem_url=redeem_url,
|
||||
active_redeem=active_redeem,
|
||||
redeem_date=game.redeem_date.strftime('%Y-%m-%d') if game.redeem_date else '')
|
||||
|
||||
|
||||
@app.route('/delete/<int:game_id>', methods=['POST'])
|
||||
@login_required
|
||||
def delete_game(game_id):
|
||||
game = db.session.get(Game, game_id)
|
||||
if not game or game.owner != current_user:
|
||||
abort(404)
|
||||
|
||||
game = Game.query.get_or_404(game_id)
|
||||
if game.owner != current_user:
|
||||
abort(403)
|
||||
|
||||
return _("Not allowed!"), 403
|
||||
try:
|
||||
db.session.delete(game)
|
||||
db.session.commit()
|
||||
flash(_('Game deleted!'), 'success')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
|
||||
flash(_('Error deleting: ') + str(e), 'danger')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# --- Import/Export Funktionen ---
|
||||
|
||||
@app.route('/export', methods=['GET'])
|
||||
@login_required
|
||||
|
@ -349,22 +206,14 @@ def export_games():
|
|||
games = Game.query.filter_by(user_id=current_user.id).all()
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
writer.writerow(['Name', 'Steam Key', 'Status', 'Recipient', 'Notes', 'URL', 'Created', 'Redeem by', 'Steam AppID'])
|
||||
|
||||
for game in games:
|
||||
writer.writerow([
|
||||
game.name,
|
||||
game.steam_key,
|
||||
game.status,
|
||||
game.recipient,
|
||||
game.notes,
|
||||
game.url,
|
||||
game.created_at.strftime('%Y-%m-%d %H:%M:%S') if game.created_at else '',
|
||||
game.name, game.steam_key, game.status, game.recipient, game.notes,
|
||||
game.url, game.created_at.strftime('%Y-%m-%d %H:%M:%S') if game.created_at else '',
|
||||
game.redeem_date.strftime('%Y-%m-%d') if game.redeem_date else '',
|
||||
game.steam_appid
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return send_file(
|
||||
io.BytesIO(output.getvalue().encode('utf-8')),
|
||||
|
@ -373,109 +222,18 @@ def export_games():
|
|||
download_name='games_export.csv'
|
||||
)
|
||||
|
||||
|
||||
@app.route('/export_pdf')
|
||||
@login_required
|
||||
def export_pdf():
|
||||
excluded_statuses = ['eingelöst', 'verschenkt']
|
||||
|
||||
games = Game.query.filter(
|
||||
Game.user_id == current_user.id,
|
||||
Game.status.notin_(excluded_statuses)
|
||||
).order_by(Game.created_at.desc()).all()
|
||||
|
||||
buffer = io.BytesIO()
|
||||
doc = SimpleDocTemplate(buffer,
|
||||
pagesize=landscape(A4),
|
||||
leftMargin=40,
|
||||
rightMargin=40,
|
||||
topMargin=40,
|
||||
bottomMargin=40
|
||||
)
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
elements = []
|
||||
img_height = 2*cm
|
||||
|
||||
# Titel
|
||||
elements.append(Paragraph(_("Game List (without Keys)"), styles['Title']))
|
||||
elements.append(Spacer(1, 12))
|
||||
|
||||
# Tabellenkopf
|
||||
col_widths = [
|
||||
5*cm, 10*cm, 6*cm, 3*cm
|
||||
]
|
||||
data = [[
|
||||
Paragraph('<b>Cover</b>', styles['Normal']),
|
||||
Paragraph('<b>Name</b>', styles['Normal']),
|
||||
Paragraph('<b>Shop-Link</b>', styles['Normal']),
|
||||
Paragraph('<b>Einlösen bis</b>', styles['Normal'])
|
||||
]]
|
||||
|
||||
for game in games:
|
||||
img = None
|
||||
if game.steam_appid:
|
||||
try:
|
||||
img_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{game.steam_appid}/header.jpg"
|
||||
img_data = io.BytesIO(requests.get(img_url, timeout=5).content)
|
||||
img = Image(img_data, width=3*cm, height=img_height)
|
||||
except Exception:
|
||||
img = Paragraph('', styles['Normal'])
|
||||
|
||||
data.append([
|
||||
img or '',
|
||||
Paragraph(game.name, styles['Normal']),
|
||||
Paragraph(game.url or '', styles['Normal']),
|
||||
game.redeem_date.strftime('%d.%m.%y') if game.redeem_date else ''
|
||||
])
|
||||
|
||||
# Tabelle formatieren
|
||||
table = Table(data, colWidths=col_widths, repeatRows=1)
|
||||
table.setStyle(TableStyle([
|
||||
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0,0), (-1,0), 8),
|
||||
('FONTSIZE', (0,1), (-1,-1), 8),
|
||||
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
|
||||
('ALIGN', (0,0), (-1,-1), 'LEFT'),
|
||||
('GRID', (0,0), (-1,-1), 0.5, colors.lightgrey),
|
||||
('WORDWRAP', (1,1), (1,-1), 'CJK'),
|
||||
]))
|
||||
|
||||
elements.append(table)
|
||||
doc.build(elements)
|
||||
|
||||
buffer.seek(0)
|
||||
return send_file(
|
||||
buffer,
|
||||
mimetype='application/pdf',
|
||||
as_attachment=True,
|
||||
download_name=f'game_export_{datetime.now().strftime("%Y%m%d")}.pdf'
|
||||
)
|
||||
|
||||
@app.route('/import', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def import_games():
|
||||
if request.method == 'POST':
|
||||
file = request.files.get('file')
|
||||
|
||||
if file and file.filename.endswith('.csv'):
|
||||
stream = io.StringIO(file.stream.read().decode("UTF8"))
|
||||
stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None)
|
||||
reader = csv.DictReader(stream)
|
||||
new_games = 0
|
||||
duplicates = 0
|
||||
|
||||
try:
|
||||
with db.session.begin_nested():
|
||||
for row in reader:
|
||||
steam_key = row['Steam Key'].strip()
|
||||
|
||||
if Game.query.filter_by(steam_key=steam_key).first():
|
||||
duplicates += 1
|
||||
continue
|
||||
|
||||
game = Game(
|
||||
new_game = Game(
|
||||
name=row['Name'],
|
||||
steam_key=steam_key,
|
||||
steam_key=row['Steam Key'],
|
||||
status=row['Status'],
|
||||
recipient=row.get('Recipient', ''),
|
||||
notes=row.get('Notes', ''),
|
||||
|
@ -485,194 +243,14 @@ def import_games():
|
|||
steam_appid=row.get('Steam AppID', ''),
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
db.session.add(game)
|
||||
new_games += 1
|
||||
|
||||
db.session.add(new_game)
|
||||
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')
|
||||
|
||||
flash(_('Import erfolgreich!'), 'success')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
flash(_('Bitte eine gültige CSV-Datei hochladen.'), 'danger')
|
||||
|
||||
return render_template('import.html')
|
||||
|
||||
@app.route('/generate_redeem/<int:game_id>', methods=['POST'])
|
||||
@login_required
|
||||
def generate_redeem(game_id):
|
||||
game = db.session.get(Game, game_id)
|
||||
if not game or game.owner != current_user:
|
||||
abort(403)
|
||||
|
||||
if game.owner != current_user or game.status != 'verschenkt':
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
token = secrets.token_urlsafe(12)[:17]
|
||||
expires = datetime.utcnow() + timedelta(hours=24)
|
||||
total_hours = 24
|
||||
|
||||
RedeemToken.query.filter_by(game_id=game_id).delete()
|
||||
|
||||
new_token = RedeemToken(
|
||||
token=token,
|
||||
game_id=game_id,
|
||||
expires=expires,
|
||||
total_hours=24
|
||||
)
|
||||
|
||||
db.session.add(new_token)
|
||||
db.session.commit()
|
||||
|
||||
redeem_url = url_for('redeem_page', token=token, _external=True)
|
||||
return jsonify({'url': redeem_url})
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Redeem error: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/redeem/<token>')
|
||||
def redeem_page(token):
|
||||
redeem_token = RedeemToken.query.filter_by(token=token).first()
|
||||
|
||||
if not redeem_token:
|
||||
abort(404)
|
||||
if redeem_token.expires < datetime.utcnow():
|
||||
db.session.delete(redeem_token)
|
||||
db.session.commit()
|
||||
abort(404)
|
||||
|
||||
game = Game.query.get(redeem_token.game_id)
|
||||
redeem_token.used = True
|
||||
db.session.commit()
|
||||
|
||||
return render_template('redeem.html',
|
||||
game=game,
|
||||
redeem_token=redeem_token,
|
||||
platform_link='https://store.steampowered.com/account/registerkey?key=' if game.steam_appid else 'https://www.gog.com/redeem')
|
||||
|
||||
# Benachrichtigungsfunktionen
|
||||
def send_pushover_notification(user, game):
|
||||
"""Sendet Pushover-Benachrichtigung für ablaufenden Key"""
|
||||
if not app.config['PUSHOVER_APP_TOKEN'] or not app.config['PUSHOVER_USER_KEY']:
|
||||
return False
|
||||
|
||||
payload = {
|
||||
"token": os.getenv('PUSHOVER_APP_TOKEN'),
|
||||
"user": os.getenv('PUSHOVER_USER_KEY'),
|
||||
"title": "Steam-Key läuft ab!",
|
||||
"message": f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!",
|
||||
"url": url_for('edit_game', game_id=game.id, _external=True),
|
||||
"url_title": "Zum Spiel",
|
||||
"priority": 1
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://api.pushover.net/1/messages.json',
|
||||
data=payload
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
app.logger.error(f"Pushover error: {str(e)}")
|
||||
return False
|
||||
|
||||
def send_gotify_notification(user, game):
|
||||
"""Sendet Gotify-Benachrichtigung für ablaufenden Key"""
|
||||
if not GOTIFY_URL or not GOTIFY_TOKEN:
|
||||
return False
|
||||
|
||||
payload = {
|
||||
"title": "Steam-Key läuft ab!",
|
||||
"message": f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!",
|
||||
"priority": 5
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{GOTIFY_URL}/message?token={GOTIFY_TOKEN}",
|
||||
json=payload
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
app.logger.error(f"Gotify error: {str(e)}")
|
||||
return False
|
||||
|
||||
def send_matrix_notification(user, game):
|
||||
"""Sendet Matrix-Benachrichtigung für ablaufenden Key"""
|
||||
if not MATRIX_HOMESERVER or not MATRIX_ACCESS_TOKEN or not MATRIX_ROOM_ID:
|
||||
return False
|
||||
|
||||
try:
|
||||
from matrix_client.client import MatrixClient
|
||||
|
||||
client = MatrixClient(MATRIX_HOMESERVER, token=MATRIX_ACCESS_TOKEN)
|
||||
room = client.join_room(MATRIX_ROOM_ID)
|
||||
|
||||
message = f"🎮 Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!"
|
||||
room.send_text(message)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
app.logger.error(f"Matrix error: {str(e)}")
|
||||
return False
|
||||
|
||||
def send_notification(user, game):
|
||||
"""Sendet Benachrichtigung über den bevorzugten Dienst des Benutzers"""
|
||||
if user.notification_service == 'pushover':
|
||||
return send_pushover_notification(user, game)
|
||||
elif user.notification_service == 'gotify':
|
||||
return send_gotify_notification(user, game)
|
||||
elif user.notification_service == 'matrix':
|
||||
return send_matrix_notification(user, game)
|
||||
return False
|
||||
|
||||
def check_expiring_keys():
|
||||
with app.app_context():
|
||||
now = datetime.utcnow()
|
||||
expiry_threshold = now + timedelta(hours=48)
|
||||
|
||||
# Moderner Select-Aufruf
|
||||
stmt = select(Game).where(
|
||||
Game.status != 'eingelöst',
|
||||
Game.redeem_date <= expiry_threshold,
|
||||
Game.redeem_date > now
|
||||
)
|
||||
|
||||
expiring_games = db.session.execute(stmt).scalars().all()
|
||||
|
||||
for game in expiring_games:
|
||||
user = User.query.get(game.user_id)
|
||||
if user.notification_service and user.notification_service != 'none':
|
||||
send_notification(user, game)
|
||||
|
||||
|
||||
# Optional: Cleanup-Funktion für regelmäßiges Löschen abgelaufener Tokens
|
||||
def cleanup_expired_tokens():
|
||||
now = datetime.utcnow()
|
||||
expired = RedeemToken.query.filter(RedeemToken.expires < now).all()
|
||||
for token in expired:
|
||||
db.session.delete(token)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# Scheduler initialisieren und starten
|
||||
scheduler = BackgroundScheduler()
|
||||
scheduler.add_job(func=check_expiring_keys, trigger="interval", hours=interval_hours)
|
||||
scheduler.add_job(func=cleanup_expired_tokens, trigger="interval", hours=1)
|
||||
scheduler.start()
|
||||
|
||||
# Shutdown des Schedulers bei Beendigung der App
|
||||
atexit.register(lambda: scheduler.shutdown())
|
||||
|
||||
if __name__ == '__main__':
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
[python: **.py]
|
||||
[jinja2: **/templates/**.html]
|
||||
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
||||
[jinja2: templates/**.html]
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
steam-manager:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- REGISTRATION_ENABLED=True
|
||||
- TZ=
|
||||
volumes:
|
||||
- ../data:/app/data
|
||||
- ../translations:/app/translations
|
||||
- ../.env:/app/.env
|
||||
user: "1000:1000"
|
||||
- /root/test/data:/app/data
|
||||
- /root/test/steam-translations:/app/translations
|
||||
environment:
|
||||
- FLASK_DEBUG=0
|
||||
restart: unless-stopped
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
flask
|
||||
flask-login
|
||||
flask-wtf
|
||||
flask-migrate
|
||||
werkzeug
|
||||
python-dotenv
|
||||
flask-sqlalchemy
|
||||
flask-babel
|
||||
jinja2<3.1.0
|
||||
itsdangerous
|
||||
sqlalchemy
|
||||
apscheduler
|
||||
matrix-client
|
||||
reportlab
|
||||
requests
|
||||
pillow
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 135 KiB |
|
@ -31,32 +31,3 @@ body {
|
|||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
}
|
||||
#expiry-countdown {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
color: #dc3545;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] #expiry-countdown {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* Progressbar-Animationen */
|
||||
#expiry-bar {
|
||||
transition: width 1s linear, background-color 0.5s ease;
|
||||
}
|
||||
|
||||
.bg-success { background-color: #198754 !important; }
|
||||
.bg-warning { background-color: #ffc107 !important; }
|
||||
.bg-danger { background-color: #dc3545 !important; }
|
||||
|
||||
.progress-bar {
|
||||
transition: width 1s linear, background-color 0.3s ease;
|
||||
}
|
||||
.table-pdf {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.table-pdf td, .table-pdf th {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
<div class="card p-4 shadow-sm">
|
||||
<h2 class="mb-4">{{ _('Add New Game') }}</h2>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ _('Name') }} *</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ _('Game Key') }} *</label>
|
||||
<label class="form-label">{{ _('Steam Key') }} *</label>
|
||||
<input type="text" name="steam_key" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
|
|
|
@ -3,18 +3,14 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>{{ _('Game Key Manager') }}</title>
|
||||
<title>{{ _('Steam Manager') }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand 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>
|
||||
<a class="navbar-brand" href="/">{{ _('Steam Manager') }}</a>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<form class="d-flex" action="{{ url_for('index') }}" method="GET">
|
||||
<input class="form-control me-2"
|
||||
|
@ -30,22 +26,28 @@
|
|||
id="darkModeSwitch" {% if theme == 'dark' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="darkModeSwitch">{{ _('Dark Mode') }}</label>
|
||||
</div>
|
||||
<!-- Sprachumschalter -->
|
||||
<div class="dropdown ms-3">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% if get_locale() == 'de' %} Deutsch {% elif get_locale() == 'en' %} English {% else %} Sprache {% endif %}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item {% if get_locale() == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li>
|
||||
<li><a class="dropdown-item {% if get_locale() == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item {% if get_locale() == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">
|
||||
Deutsch
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item {% if get_locale() == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">
|
||||
English
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Passwort') }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
|
||||
</li>
|
||||
<a href="{{ url_for('export_games') }}" class="btn btn-outline-secondary">⬇️ {{ _('Export') }}</a>
|
||||
<a href="{{ url_for('import_games') }}" class="btn btn-outline-secondary">⬆️ {{ _('Import') }}</a>
|
||||
<a href="{{ url_for('logout') }}" class="btn btn-danger ms-3">{{ _('Logout') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,16 +68,16 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const toggle = document.getElementById('darkModeSwitch')
|
||||
const html = document.documentElement
|
||||
|
||||
const toggle = document.getElementById('darkModeSwitch');
|
||||
const html = document.documentElement;
|
||||
toggle.addEventListener('change', function() {
|
||||
const theme = this.checked ? 'dark' : 'light'
|
||||
const theme = this.checked ? 'dark' : 'light';
|
||||
fetch('/set-theme/' + theme)
|
||||
.then(() => html.setAttribute('data-bs-theme', theme))
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
html.setAttribute('data-bs-theme', theme);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% include "footer.html" %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
{% 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,14 +3,13 @@
|
|||
<div class="card p-4 shadow-sm">
|
||||
<h2 class="mb-4">{{ _('Edit Game') }}</h2>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ _('Name') }} *</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ game.name }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">{{ _('Game Key') }} *</label>
|
||||
<label class="form-label">{{ _('Steam Key') }} *</label>
|
||||
<input type="text" name="steam_key" class="form-control" value="{{ game.steam_key }}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
@ -41,21 +40,6 @@
|
|||
<label class="form-label">{{ _('Notes') }}</label>
|
||||
<textarea name="notes" class="form-control" rows="3">{{ game.notes }}</textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
{% if redeem_url and active_redeem %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ _('Active Redeem Link') }}</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
value="{{ redeem_url }}"
|
||||
readonly
|
||||
onclick="this.select()">
|
||||
<small class="text-muted">
|
||||
{{ _('Expires at') }}: {{ active_redeem.expires.strftime('%d.%m.%Y %H:%M') }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">{{ _('Save') }}</button>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<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,7 +3,6 @@
|
|||
<div class="card p-4 shadow-sm">
|
||||
<h2 class="mb-4">{{ _('Import Games') }}</h2>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ _('CSV-Datei auswählen') }}</label>
|
||||
<input type="file" name="file" class="form-control" accept=".csv" required>
|
||||
|
|
|
@ -2,12 +2,9 @@
|
|||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>{{ _('My Games') }}</h1>
|
||||
<div>
|
||||
<a href="{{ url_for('export_games') }}" class="btn btn-outline-secondary">⬇️ {{ _('Export CSV') }}</a>
|
||||
<a href="{{ url_for('export_pdf') }}" class="btn btn-outline-secondary">⬇️ Export PDF (for sharing)</a>
|
||||
<a href="{{ url_for('import_games') }}" class="btn btn-outline-secondary">⬆️ {{ _('Import CSV') }}</a>
|
||||
<a href="{{ url_for('add_game') }}" class="btn btn-primary">+ {{ _('Add New Game') }}</a>
|
||||
</div>
|
||||
<a href="{{ url_for('add_game') }}" class="btn btn-primary">
|
||||
+ {{ _('Add New Game') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if games %}
|
||||
|
@ -57,16 +54,8 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{% if game.status == 'verschenkt' %}
|
||||
<button class="btn btn-sm btn-success generate-redeem"
|
||||
data-game-id="{{ game.id }}"
|
||||
title="{{ _('Generate redeem link') }}">
|
||||
🔗
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('edit_game', game_id=game.id) }}" class="btn btn-sm btn-warning">✏️</a>
|
||||
<form method="POST" action="{{ url_for('delete_game', game_id=game.id) }}" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ _('Really delete?') }}')">🗑️</button>
|
||||
</form>
|
||||
</td>
|
||||
|
@ -75,33 +64,6 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
|
||||
document.querySelectorAll('.generate-redeem').forEach(btn => {
|
||||
btn.addEventListener('click', async function() {
|
||||
const gameId = this.dataset.gameId;
|
||||
try {
|
||||
const response = await fetch('/generate_redeem/' + gameId, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
if (!response.ok) throw new Error('Network error');
|
||||
const data = await response.json();
|
||||
if(data.url) {
|
||||
await navigator.clipboard.writeText(data.url);
|
||||
alert('{{ _("Redeem link copied to clipboard!") }}');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('{{ _("Error generating link") }}');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<div class="alert alert-info">{{ _('No games yet') }}</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -3,11 +3,9 @@
|
|||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm">
|
||||
<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;">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">{{ _('Login') }}</h2>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ _('Username') }}</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
|
@ -26,4 +24,3 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
{% 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,7 +6,6 @@
|
|||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">{{ _('Register') }}</h2>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ _('Username') }}</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
|
|
Binary file not shown.
216
steam-translations/de/LC_MESSAGES/messages.po
Normal file
216
steam-translations/de/LC_MESSAGES/messages.po
Normal file
|
@ -0,0 +1,216 @@
|
|||
# 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"
|
Binary file not shown.
217
steam-translations/en/LC_MESSAGES/messages.po
Normal file
217
steam-translations/en/LC_MESSAGES/messages.po
Normal file
|
@ -0,0 +1,217 @@
|
|||
# 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 ""
|
||||
|
216
steam-translations/messages.pot
Normal file
216
steam-translations/messages.pot
Normal file
|
@ -0,0 +1,216 @@
|
|||
# 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 ""
|
||||
|
24
translate.sh
24
translate.sh
|
@ -3,26 +3,20 @@ set -e
|
|||
|
||||
cd "$(dirname "$0")/steam-gift-manager"
|
||||
|
||||
declare -A locales=(
|
||||
["de"]="de"
|
||||
["en"]="en"
|
||||
)
|
||||
|
||||
# POT-Datei erstellen
|
||||
# 1. Extrahiere alle Texte
|
||||
docker-compose exec steam-manager pybabel extract -F babel.cfg -o translations/messages.pot .
|
||||
|
||||
# Für jede Sprache prüfen und ggf. initialisieren
|
||||
for lang in "${!locales[@]}"; do
|
||||
if [ ! -f "translations/${locales[$lang]}/LC_MESSAGES/messages.po" ]; then
|
||||
docker-compose exec steam-manager pybabel init \
|
||||
-i translations/messages.pot \
|
||||
-d translations \
|
||||
-l "${locales[$lang]}"
|
||||
# 2. Initialisiere Sprachen (nur einmal nötig, danach auskommentieren)
|
||||
for lang in de en; do
|
||||
if [ ! -f "../steam-translations/$lang/LC_MESSAGES/messages.po" ]; then
|
||||
docker-compose exec steam-manager pybabel init -i translations/messages.pot -d translations -l $lang
|
||||
fi
|
||||
done
|
||||
|
||||
# Übersetzungen aktualisieren und kompilieren
|
||||
# 3. Aktualisiere Übersetzungen
|
||||
docker-compose exec steam-manager pybabel update -i translations/messages.pot -d translations
|
||||
|
||||
# 4. Kompiliere Übersetzungen
|
||||
docker-compose exec steam-manager pybabel compile -d translations
|
||||
|
||||
echo "✅ Übersetzungen aktualisiert!"
|
||||
echo "✅ Übersetzungen extrahiert, aktualisiert und kompiliert!"
|
||||
|
|
|
@ -1,287 +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-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 ""
|
||||
|
|
@ -1,287 +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-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 ""
|
||||
|
|
@ -1,286 +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-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
22
upgrade.sh
|
@ -1,22 +0,0 @@
|
|||
#!/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