Ready For 1.1
This commit is contained in:
parent
c23f2f88e8
commit
7a221acb8e
Binary file not shown.
After Width: | Height: | Size: 451 KiB |
|
@ -7,7 +7,7 @@ No more confusion about whether a key is redeemed, gifted, or still unused – n
|
||||||
|
|
||||||
You can even gift your keys via a unique 24-hour website link – just mark a game as "Gifted" and copy the link from your overview. (HTTPS recommended)
|
You can even gift your keys via a unique 24-hour website link – just mark a game as "Gifted" and copy the link from your overview. (HTTPS recommended)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
211
setup.sh
211
setup.sh
|
@ -144,18 +144,18 @@ DEFAULT_LANGUAGE="en"
|
||||||
SUPPORTED_LANGUAGES="de,en"
|
SUPPORTED_LANGUAGES="de,en"
|
||||||
|
|
||||||
# Timezone
|
# Timezone
|
||||||
TZ=Europe/Berlin
|
TZ="Europe/Berlin"
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
FORCE_HTTPS=False
|
FORCE_HTTPS="False"
|
||||||
SESSION_COOKIE_SECURE=auto
|
SESSION_COOKIE_SECURE="auto"
|
||||||
CSRF_ENABLED="True"
|
CSRF_ENABLED="True"
|
||||||
|
|
||||||
# Account registration
|
# Account registration
|
||||||
REGISTRATION_ENABLED="True"
|
REGISTRATION_ENABLED="True"
|
||||||
|
|
||||||
# checking interval if keys have to be redeemed before a specific date
|
# checking interval if keys have to be redeemed before a specific date
|
||||||
CHECK_EXPIRING_KEYS_INTERVAL_HOURS=6
|
CHECK_EXPIRING_KEYS_INTERVAL_HOURS="6"
|
||||||
|
|
||||||
# Want to check prices? Here you are!
|
# Want to check prices? Here you are!
|
||||||
ITAD_API_KEY="your-secret-key-here"
|
ITAD_API_KEY="your-secret-key-here"
|
||||||
|
@ -170,7 +170,7 @@ APPRISE_URLS=""
|
||||||
#matrixs://TOKEN@matrix.org/!ROOM_ID"
|
#matrixs://TOKEN@matrix.org/!ROOM_ID"
|
||||||
|
|
||||||
# Redis URL
|
# Redis URL
|
||||||
REDIS_URL=redis://redis:6379/0
|
REDIS_URL="redis://redis:6379/0"
|
||||||
|
|
||||||
# Enable Debug (e.g. for VS Code)
|
# Enable Debug (e.g. for VS Code)
|
||||||
FLASK_DEBUG=1
|
FLASK_DEBUG=1
|
||||||
|
@ -253,6 +253,7 @@ from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.exc import IntegrityError, LegacyAPIWarning
|
from sqlalchemy.exc import IntegrityError, LegacyAPIWarning
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from wtforms import SelectField, StringField, TextAreaField, validators
|
from wtforms import SelectField, StringField, TextAreaField, validators
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
|
@ -281,6 +282,11 @@ app.jinja_env.globals['getattr'] = getattr
|
||||||
def not_found_error(error):
|
def not_found_error(error):
|
||||||
return render_template('404.html'), 404
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
app.wsgi_app = ProxyFix(
|
||||||
|
app.wsgi_app,
|
||||||
|
x_proto=1, # Trust X-Forwarded-Proto Header
|
||||||
|
x_host=1 # Trust X-Forwarded-Host Header
|
||||||
|
)
|
||||||
|
|
||||||
# UNIX-Systems (Linux, Docker)
|
# UNIX-Systems (Linux, Docker)
|
||||||
try:
|
try:
|
||||||
|
@ -1162,7 +1168,7 @@ def generate_redeem(game_id):
|
||||||
)
|
)
|
||||||
db.session.add(new_token)
|
db.session.add(new_token)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
redeem_url = url_for('redeem', token=token, _external=True)
|
redeem_url = url_for('redeem', token=token, _external=True, _scheme='https')
|
||||||
message = translate(
|
message = translate(
|
||||||
'Redeem link generated: <a href="{url}" target="_blank">{url}</a>',
|
'Redeem link generated: <a href="{url}" target="_blank">{url}</a>',
|
||||||
url=redeem_url
|
url=redeem_url
|
||||||
|
@ -1186,22 +1192,25 @@ def redeem_page(token):
|
||||||
redeem_token.used = True
|
redeem_token.used = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# which Plattform
|
if game.platform == 'steam':
|
||||||
if game.platform == "steam" or game.steam_appid:
|
|
||||||
platform_link = 'https://store.steampowered.com/account/registerkey?key='
|
platform_link = 'https://store.steampowered.com/account/registerkey?key='
|
||||||
platform_label = "Steam"
|
platform_name = 'Steam'
|
||||||
elif game.platform == "gog":
|
elif game.platform == 'gog':
|
||||||
platform_link = 'https://www.gog.com/redeem/'
|
platform_link = 'https://www.gog.com/redeem/'
|
||||||
platform_label = "GOG"
|
platform_name = 'GOG'
|
||||||
elif game.platform == "xbox":
|
elif game.platform == 'xbox':
|
||||||
platform_link = 'https://redeem.microsoft.com/'
|
platform_link = 'https://redeem.microsoft.com/'
|
||||||
platform_label = "XBOX"
|
platform_name = 'Xbox'
|
||||||
elif game.platform == "playstation":
|
elif game.platform == 'playstation':
|
||||||
platform_link = 'https://store.playstation.com/redeem'
|
platform_link = 'https://redeem.playstation.com/'
|
||||||
platform_label = "PlayStation"
|
platform_name = 'PlayStation'
|
||||||
|
elif game.platform == 'switch':
|
||||||
|
platform_link = 'https://ec.nintendo.com/redeem/'
|
||||||
|
platform_name = 'Nintendo Switch'
|
||||||
else:
|
else:
|
||||||
platform_link = '#'
|
# Fallback für benutzerdefinierte Keys
|
||||||
platform_label = game.platform.capitalize() if game.platform else "Unknown"
|
platform_link = ''
|
||||||
|
platform_name = 'Key'
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'redeem.html',
|
'redeem.html',
|
||||||
|
@ -1209,9 +1218,10 @@ def redeem_page(token):
|
||||||
redeem_token=redeem_token,
|
redeem_token=redeem_token,
|
||||||
expires_timestamp=int(expires_utc.timestamp() * 1000),
|
expires_timestamp=int(expires_utc.timestamp() * 1000),
|
||||||
platform_link=platform_link,
|
platform_link=platform_link,
|
||||||
platform_label=platform_label
|
platform_name=platform_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/users')
|
@app.route('/admin/users')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
|
@ -1524,6 +1534,7 @@ cat <<HTML_END > templates/base.html
|
||||||
<meta name="theme-color" content="#212529">
|
<meta name="theme-color" content="#212529">
|
||||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||||
<title>{{ _('Game Key Manager') }}</title>
|
<title>{{ _('Game Key Manager') }}</title>
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
<!-- Preload Bootstrap CSS for better LCP -->
|
<!-- Preload Bootstrap CSS for better LCP -->
|
||||||
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||||
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></noscript>
|
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></noscript>
|
||||||
|
@ -1565,26 +1576,34 @@ cat <<HTML_END > templates/base.html
|
||||||
<input class="form-control me-2" type="search" name="q" id="searchInput" placeholder="{{ _('Search') }}" value="{{ search_query }}">
|
<input class="form-control me-2" type="search" name="q" id="searchInput" placeholder="{{ _('Search') }}" value="{{ search_query }}">
|
||||||
<button class="btn btn-outline-success" type="submit" aria-label="{{ _('Search') }}">🔍</button>
|
<button class="btn btn-outline-success" type="submit" aria-label="{{ _('Search') }}">🔍</button>
|
||||||
</form>
|
</form>
|
||||||
<ul class="navbar-nav ms-lg-3 mb-2 mb-lg-0">
|
<div class="dropdown ms-3">
|
||||||
<li class="nav-item dropdown">
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="langDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
{% if session.get('lang', 'en') == 'de' %} Deutsch {% elif session.get('lang', 'en') == 'en' %} English {% else %} Sprache {% endif %}
|
{% if session.get('lang', 'en') == 'de' %} Deutsch {% elif session.get('lang', 'en') == 'en' %} English {% else %} Sprache {% endif %}
|
||||||
</a>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="langDropdown">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item {% if session.get('lang', 'en') == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li>
|
<li><a class="dropdown-item {% if session.get('lang', 'en') == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li>
|
||||||
<li><a class="dropdown-item {% if session.get('lang', 'en') == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li>
|
<li><a class="dropdown-item {% if session.get('lang', 'en') == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
{% if current_user.is_admin %}
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_users') }}">⚙️ {{ _('Admin') }}</a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_audit_logs') }}">📜 {{ _('Audit Logs') }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Password') }}</a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<div class="dropdown ms-3">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{{ _('Import/Export') }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('export_games') }}">⬇️ {{ _('Export CSV') }}</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('export_pdf') }}">⬇️ Export PDF (for sharing)</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('import_games') }}">⬆️ {{ _('Import CSV') }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<a class="btn btn-outline-secondary ms-3" href="{{ url_for('admin_users') }}">⚙️ {{ _('Admin') }}</a>
|
||||||
|
<a class="btn btn-outline-secondary ms-1" href="{{ url_for('admin_audit_logs') }}">📜 {{ _('Audit Logs') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
<a class="btn btn-outline-secondary ms-3" href="{{ url_for('change_password') }}">🔒 {{ _('Password') }}</a>
|
||||||
|
<a class="btn btn-outline-danger ms-1" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
@ -1652,16 +1671,7 @@ HTML_END
|
||||||
cat <<'HTML_END' > templates/index.html
|
cat <<'HTML_END' > templates/index.html
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<button id="toggle-keys" class="btn btn-sm btn-outline-secondary mb-2">{{ _('Show/Hide Keys') }}</button>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if games %}
|
{% if games %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
|
@ -1669,7 +1679,7 @@ cat <<'HTML_END' > templates/index.html
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ _('Cover') }}</th>
|
<th>{{ _('Cover') }}</th>
|
||||||
<th>{{ _('Name') }}</th>
|
<th>{{ _('Name') }}</th>
|
||||||
<th>{{ _('Key') }}</th>
|
<th class="key-col d-md-table-cell">{{ _('Key') }}</th>
|
||||||
<th>{{ _('Status') }}</th>
|
<th>{{ _('Status') }}</th>
|
||||||
<th>{{ _('Created') }}</th>
|
<th>{{ _('Created') }}</th>
|
||||||
<th>{{ _('Redeem by') }}</th>
|
<th>{{ _('Redeem by') }}</th>
|
||||||
|
@ -1685,15 +1695,15 @@ cat <<'HTML_END' > templates/index.html
|
||||||
<a href="{{ url_for('game_details', game_id=game.id) }}" title="{{ _('Details') }}">
|
<a href="{{ url_for('game_details', game_id=game.id) }}" title="{{ _('Details') }}">
|
||||||
{% if game.steam_appid %}
|
{% if game.steam_appid %}
|
||||||
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
|
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
|
||||||
alt="Steam Header"
|
alt="Steam Header"
|
||||||
class="game-cover"
|
class="game-cover"
|
||||||
{% if loop.first %}fetchpriority="high"{% endif %}
|
{% if loop.first %}fetchpriority="high"{% endif %}
|
||||||
width="368"
|
width="368"
|
||||||
height="172"
|
height="172"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
{% elif game.url and 'gog.com' in game.url %}
|
{% elif game.url and 'gog.com' in game.url %}
|
||||||
<img src="{{ url_for('static', filename='gog_logo.webp') }}"
|
<img src="{{ url_for('static', filename='gog_logo.webp') }}"
|
||||||
alt="GOG Logo"
|
alt="GOG Logo"
|
||||||
class="game-cover"
|
class="game-cover"
|
||||||
width="368"
|
width="368"
|
||||||
height="172"
|
height="172"
|
||||||
|
@ -1702,7 +1712,7 @@ cat <<'HTML_END' > templates/index.html
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ game.name }}</td>
|
<td>{{ game.name }}</td>
|
||||||
<td class="font-monospace">{{ game.steam_key }}</td>
|
<td class="font-monospace key-col d-none d-md-table-cell">{{ game.steam_key }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if game.status == 'nicht eingelöst' %}
|
{% if game.status == 'nicht eingelöst' %}
|
||||||
<span class="badge bg-warning text-dark">{{ _('Not redeemed') }}</span>
|
<span class="badge bg-warning text-dark">{{ _('Not redeemed') }}</span>
|
||||||
|
@ -1752,8 +1762,8 @@ cat <<'HTML_END' > templates/index.html
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
{% if game.status == 'geschenkt' %}
|
{% if game.status == 'geschenkt' %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-success generate-redeem"
|
class="btn btn-sm btn-success generate-redeem"
|
||||||
data-game-id="{{ game.id }}"
|
data-game-id="{{ game.id }}"
|
||||||
title="{{ _('Generate redeem link') }}">
|
title="{{ _('Generate redeem link') }}">
|
||||||
🔗
|
🔗
|
||||||
|
@ -1774,7 +1784,7 @@ cat <<'HTML_END' > templates/index.html
|
||||||
document.querySelectorAll('.generate-redeem').forEach(btn => {
|
document.querySelectorAll('.generate-redeem').forEach(btn => {
|
||||||
btn.addEventListener('click', async function() {
|
btn.addEventListener('click', async function() {
|
||||||
const gameId = this.dataset.gameId;
|
const gameId = this.dataset.gameId;
|
||||||
const flashContainer = document.querySelector('.flash-container');
|
const flashContainer = document.querySelector('.flash-container')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/generate_redeem/${gameId}`, {
|
const response = await fetch(`/generate_redeem/${gameId}`, {
|
||||||
|
@ -1786,22 +1796,36 @@ document.querySelectorAll('.generate-redeem').forEach(btn => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.error || '{{ _("Unknown error") }}');
|
throw new Error(data.error || '{{ _("Unknown error") }}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.url) {
|
if (data.url) {
|
||||||
await navigator.clipboard.writeText(data.url);
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(data.url)
|
||||||
// Erfolgsmeldung mit übersetztem Text
|
.then(() => {
|
||||||
flashContainer.innerHTML = `
|
// Succcess ?? maybe
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
flashContainer.innerHTML = `
|
||||||
{{ _("Link copied") }}: <a href="${data.url}" target="_blank">${data.url}</a>
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
{{ _("Link copied") }}: <a href="${data.url}" target="_blank">${data.url}</a>
|
||||||
</div>
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
`;
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
flashContainer.innerHTML = `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
{{ _("Clipboard error") }}: ${err.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert("Clipboard API is not supported in your browser or context.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fehlermeldung mit übersetztem Text
|
// Fehlermeldung mit übersetztem Text
|
||||||
flashContainer.innerHTML = `
|
flashContainer.innerHTML = `
|
||||||
|
@ -1814,10 +1838,29 @@ document.querySelectorAll('.generate-redeem').forEach(btn => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log("DOM ist geladen!"); // Überprüfe, ob DOMContentLoaded überhaupt ausgeführt wird
|
||||||
|
const toggleKeysButton = document.getElementById('toggle-keys');
|
||||||
|
if (toggleKeysButton) {
|
||||||
|
console.log("Button with ID 'toggle-keys' found!");
|
||||||
|
toggleKeysButton.addEventListener('click', function() {
|
||||||
|
console.log("Button clicked!");
|
||||||
|
const keyCols = document.querySelectorAll('.key-col');
|
||||||
|
keyCols.forEach(function(el) {
|
||||||
|
el.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Button with ID 'toggle-keys' not found!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-info">{{ _('No games yet') }}</div>
|
<div class="alert alert-info">{{ _('No games yet') }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
HTML_END
|
HTML_END
|
||||||
|
|
||||||
# Login Template
|
# Login Template
|
||||||
|
@ -2287,11 +2330,17 @@ cat <<'HTML_END' > templates/redeem.html
|
||||||
<h4>{{ _('Your Key:') }}</h4>
|
<h4>{{ _('Your Key:') }}</h4>
|
||||||
<code class="fs-3">{{ game.steam_key }}</code>
|
<code class="fs-3">{{ game.steam_key }}</code>
|
||||||
</div>
|
</div>
|
||||||
|
{% if platform_link %}
|
||||||
<a href="{{ platform_link }}{{ game.steam_key }}"
|
<a href="{{ platform_link }}{{ game.steam_key }}"
|
||||||
class="btn btn-primary btn-lg mb-3"
|
class="btn btn-primary btn-lg mb-3"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
{{ _('Redeem now on') }} {{ platform_label }}
|
{{ _('Redeem now on') }} {{ platform_name }}
|
||||||
</a>
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{{ _('Your key:') }} <code class="fs-3">{{ game.steam_key }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="mt-4 text-muted">
|
<div class="mt-4 text-muted">
|
||||||
<small>
|
<small>
|
||||||
{{ _('This page will expire in') }}
|
{{ _('This page will expire in') }}
|
||||||
|
@ -2299,9 +2348,9 @@ cat <<'HTML_END' > templates/redeem.html
|
||||||
</small>
|
</small>
|
||||||
<div class="progress mt-2" style="height: 8px;">
|
<div class="progress mt-2" style="height: 8px;">
|
||||||
<div id="expiry-bar"
|
<div id="expiry-bar"
|
||||||
class="progress-bar bg-danger"
|
class="progress-bar bg-danger"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: 100%">
|
style="width: 100%">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2554,6 +2603,7 @@ cat <<HTML_END > templates/admin_audit_logs.html
|
||||||
HTML_END
|
HTML_END
|
||||||
|
|
||||||
# Error Sites
|
# Error Sites
|
||||||
|
# 403 Tempate
|
||||||
cat <<HTML_END > templates/403.html
|
cat <<HTML_END > templates/403.html
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -2582,6 +2632,7 @@ cat <<HTML_END > templates/403.html
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
HTML_END
|
HTML_END
|
||||||
|
|
||||||
|
# 404 Template
|
||||||
cat <<HTML_END > templates/404.html
|
cat <<HTML_END > templates/404.html
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -2778,6 +2829,28 @@ td.font-monospace {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.key-col.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.key-col {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .btn,
|
||||||
|
.navbar .dropdown-toggle,
|
||||||
|
.navbar .nav-link {
|
||||||
|
min-height: 40px;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
|
.alert-error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
|
||||||
.alert-success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
|
.alert-success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
|
||||||
.alert-info { background: #d9edf7; color: #31708f; }
|
.alert-info { background: #d9edf7; color: #31708f; }
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
# Flask-Configuration
|
# Flask-Configuration
|
||||||
SECRET_KEY=""
|
SECRET_KEY="1dc3d95006f7466670ac2d705ce43dc4a5ad8e2189dbe539"
|
||||||
REDEEM_SECRET=""
|
REDEEM_SECRET="a50a961667ded234b1e59532ab7e27e1"
|
||||||
WTF_CSRF_SECRET_KEY=""
|
WTF_CSRF_SECRET_KEY="845ae46bd1bea30311e98df232d78b4e"
|
||||||
|
|
||||||
# Language Settings
|
# Language Settings
|
||||||
DEFAULT_LANGUAGE="en"
|
DEFAULT_LANGUAGE="en"
|
||||||
SUPPORTED_LANGUAGES="de,en"
|
SUPPORTED_LANGUAGES="de,en"
|
||||||
|
|
||||||
# Timezone
|
# Timezone
|
||||||
TZ=Europe/Berlin
|
TZ="Europe/Berlin"
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
FORCE_HTTPS=False
|
FORCE_HTTPS="False"
|
||||||
SESSION_COOKIE_SECURE=auto
|
SESSION_COOKIE_SECURE="auto"
|
||||||
CSRF_ENABLED="True"
|
CSRF_ENABLED="True"
|
||||||
|
|
||||||
# Account registration
|
# Account registration
|
||||||
REGISTRATION_ENABLED="True"
|
REGISTRATION_ENABLED="True"
|
||||||
|
|
||||||
# checking interval if keys have to be redeemed before a specific date
|
# checking interval if keys have to be redeemed before a specific date
|
||||||
CHECK_EXPIRING_KEYS_INTERVAL_HOURS=6
|
CHECK_EXPIRING_KEYS_INTERVAL_HOURS="6"
|
||||||
|
|
||||||
# Want to check prices? Here you are!
|
# Want to check prices? Here you are!
|
||||||
ITAD_API_KEY="your-secret-key-here"
|
ITAD_API_KEY="your-secret-key-here"
|
||||||
|
@ -34,7 +34,7 @@ APPRISE_URLS=""
|
||||||
#matrixs://TOKEN@matrix.org/!ROOM_ID"
|
#matrixs://TOKEN@matrix.org/!ROOM_ID"
|
||||||
|
|
||||||
# Redis URL
|
# Redis URL
|
||||||
REDIS_URL=redis://redis:6379/0
|
REDIS_URL="redis://redis:6379/0"
|
||||||
|
|
||||||
# Enable Debug (e.g. for VS Code)
|
# Enable Debug (e.g. for VS Code)
|
||||||
FLASK_DEBUG=1
|
FLASK_DEBUG=1
|
||||||
|
|
|
@ -72,6 +72,7 @@ from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.exc import IntegrityError, LegacyAPIWarning
|
from sqlalchemy.exc import IntegrityError, LegacyAPIWarning
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from wtforms import SelectField, StringField, TextAreaField, validators
|
from wtforms import SelectField, StringField, TextAreaField, validators
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
|
@ -100,6 +101,11 @@ app.jinja_env.globals['getattr'] = getattr
|
||||||
def not_found_error(error):
|
def not_found_error(error):
|
||||||
return render_template('404.html'), 404
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
app.wsgi_app = ProxyFix(
|
||||||
|
app.wsgi_app,
|
||||||
|
x_proto=1, # Trust X-Forwarded-Proto Header
|
||||||
|
x_host=1 # Trust X-Forwarded-Host Header
|
||||||
|
)
|
||||||
|
|
||||||
# UNIX-Systems (Linux, Docker)
|
# UNIX-Systems (Linux, Docker)
|
||||||
try:
|
try:
|
||||||
|
@ -981,7 +987,7 @@ def generate_redeem(game_id):
|
||||||
)
|
)
|
||||||
db.session.add(new_token)
|
db.session.add(new_token)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
redeem_url = url_for('redeem', token=token, _external=True)
|
redeem_url = url_for('redeem', token=token, _external=True, _scheme='https')
|
||||||
message = translate(
|
message = translate(
|
||||||
'Redeem link generated: <a href="{url}" target="_blank">{url}</a>',
|
'Redeem link generated: <a href="{url}" target="_blank">{url}</a>',
|
||||||
url=redeem_url
|
url=redeem_url
|
||||||
|
@ -1005,22 +1011,25 @@ def redeem_page(token):
|
||||||
redeem_token.used = True
|
redeem_token.used = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# which Plattform
|
if game.platform == 'steam':
|
||||||
if game.platform == "steam" or game.steam_appid:
|
|
||||||
platform_link = 'https://store.steampowered.com/account/registerkey?key='
|
platform_link = 'https://store.steampowered.com/account/registerkey?key='
|
||||||
platform_label = "Steam"
|
platform_name = 'Steam'
|
||||||
elif game.platform == "gog":
|
elif game.platform == 'gog':
|
||||||
platform_link = 'https://www.gog.com/redeem/'
|
platform_link = 'https://www.gog.com/redeem/'
|
||||||
platform_label = "GOG"
|
platform_name = 'GOG'
|
||||||
elif game.platform == "xbox":
|
elif game.platform == 'xbox':
|
||||||
platform_link = 'https://redeem.microsoft.com/'
|
platform_link = 'https://redeem.microsoft.com/'
|
||||||
platform_label = "XBOX"
|
platform_name = 'Xbox'
|
||||||
elif game.platform == "playstation":
|
elif game.platform == 'playstation':
|
||||||
platform_link = 'https://store.playstation.com/redeem'
|
platform_link = 'https://redeem.playstation.com/'
|
||||||
platform_label = "PlayStation"
|
platform_name = 'PlayStation'
|
||||||
|
elif game.platform == 'switch':
|
||||||
|
platform_link = 'https://ec.nintendo.com/redeem/'
|
||||||
|
platform_name = 'Nintendo Switch'
|
||||||
else:
|
else:
|
||||||
platform_link = '#'
|
# Fallback für benutzerdefinierte Keys
|
||||||
platform_label = game.platform.capitalize() if game.platform else "Unknown"
|
platform_link = ''
|
||||||
|
platform_name = 'Key'
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'redeem.html',
|
'redeem.html',
|
||||||
|
@ -1028,9 +1037,10 @@ def redeem_page(token):
|
||||||
redeem_token=redeem_token,
|
redeem_token=redeem_token,
|
||||||
expires_timestamp=int(expires_utc.timestamp() * 1000),
|
expires_timestamp=int(expires_utc.timestamp() * 1000),
|
||||||
platform_link=platform_link,
|
platform_link=platform_link,
|
||||||
platform_label=platform_label
|
platform_name=platform_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/users')
|
@app.route('/admin/users')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
|
|
|
@ -164,6 +164,28 @@ td.font-monospace {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.key-col.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.key-col {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .btn,
|
||||||
|
.navbar .dropdown-toggle,
|
||||||
|
.navbar .nav-link {
|
||||||
|
min-height: 40px;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
|
.alert-error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
|
||||||
.alert-success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
|
.alert-success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
|
||||||
.alert-info { background: #d9edf7; color: #31708f; }
|
.alert-info { background: #d9edf7; color: #31708f; }
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<meta name="theme-color" content="#212529">
|
<meta name="theme-color" content="#212529">
|
||||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||||
<title>{{ _('Game Key Manager') }}</title>
|
<title>{{ _('Game Key Manager') }}</title>
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
<!-- Preload Bootstrap CSS for better LCP -->
|
<!-- Preload Bootstrap CSS for better LCP -->
|
||||||
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||||
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></noscript>
|
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></noscript>
|
||||||
|
@ -49,26 +50,34 @@
|
||||||
<input class="form-control me-2" type="search" name="q" id="searchInput" placeholder="{{ _('Search') }}" value="{{ search_query }}">
|
<input class="form-control me-2" type="search" name="q" id="searchInput" placeholder="{{ _('Search') }}" value="{{ search_query }}">
|
||||||
<button class="btn btn-outline-success" type="submit" aria-label="{{ _('Search') }}">🔍</button>
|
<button class="btn btn-outline-success" type="submit" aria-label="{{ _('Search') }}">🔍</button>
|
||||||
</form>
|
</form>
|
||||||
<ul class="navbar-nav ms-lg-3 mb-2 mb-lg-0">
|
<div class="dropdown ms-3">
|
||||||
<li class="nav-item dropdown">
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="langDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
{% if session.get('lang', 'en') == 'de' %} Deutsch {% elif session.get('lang', 'en') == 'en' %} English {% else %} Sprache {% endif %}
|
{% if session.get('lang', 'en') == 'de' %} Deutsch {% elif session.get('lang', 'en') == 'en' %} English {% else %} Sprache {% endif %}
|
||||||
</a>
|
</button>
|
||||||
<ul class="dropdown-menu" aria-labelledby="langDropdown">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item {% if session.get('lang', 'en') == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li>
|
<li><a class="dropdown-item {% if session.get('lang', 'en') == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li>
|
||||||
<li><a class="dropdown-item {% if session.get('lang', 'en') == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li>
|
<li><a class="dropdown-item {% if session.get('lang', 'en') == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
{% if current_user.is_admin %}
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_users') }}">⚙️ {{ _('Admin') }}</a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_audit_logs') }}">📜 {{ _('Audit Logs') }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Password') }}</a></li>
|
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<div class="dropdown ms-3">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{{ _('Import/Export') }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('export_games') }}">⬇️ {{ _('Export CSV') }}</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('export_pdf') }}">⬇️ Export PDF (for sharing)</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('import_games') }}">⬆️ {{ _('Import CSV') }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<a class="btn btn-outline-secondary ms-3" href="{{ url_for('admin_users') }}">⚙️ {{ _('Admin') }}</a>
|
||||||
|
<a class="btn btn-outline-secondary ms-1" href="{{ url_for('admin_audit_logs') }}">📜 {{ _('Audit Logs') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
<a class="btn btn-outline-secondary ms-3" href="{{ url_for('change_password') }}">🔒 {{ _('Password') }}</a>
|
||||||
|
<a class="btn btn-outline-danger ms-1" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<button id="toggle-keys" class="btn btn-sm btn-outline-secondary mb-2">{{ _('Show/Hide Keys') }}</button>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if games %}
|
{% if games %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
|
@ -17,7 +8,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ _('Cover') }}</th>
|
<th>{{ _('Cover') }}</th>
|
||||||
<th>{{ _('Name') }}</th>
|
<th>{{ _('Name') }}</th>
|
||||||
<th>{{ _('Key') }}</th>
|
<th class="key-col d-md-table-cell">{{ _('Key') }}</th>
|
||||||
<th>{{ _('Status') }}</th>
|
<th>{{ _('Status') }}</th>
|
||||||
<th>{{ _('Created') }}</th>
|
<th>{{ _('Created') }}</th>
|
||||||
<th>{{ _('Redeem by') }}</th>
|
<th>{{ _('Redeem by') }}</th>
|
||||||
|
@ -33,15 +24,15 @@
|
||||||
<a href="{{ url_for('game_details', game_id=game.id) }}" title="{{ _('Details') }}">
|
<a href="{{ url_for('game_details', game_id=game.id) }}" title="{{ _('Details') }}">
|
||||||
{% if game.steam_appid %}
|
{% if game.steam_appid %}
|
||||||
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
|
<img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
|
||||||
alt="Steam Header"
|
alt="Steam Header"
|
||||||
class="game-cover"
|
class="game-cover"
|
||||||
{% if loop.first %}fetchpriority="high"{% endif %}
|
{% if loop.first %}fetchpriority="high"{% endif %}
|
||||||
width="368"
|
width="368"
|
||||||
height="172"
|
height="172"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
{% elif game.url and 'gog.com' in game.url %}
|
{% elif game.url and 'gog.com' in game.url %}
|
||||||
<img src="{{ url_for('static', filename='gog_logo.webp') }}"
|
<img src="{{ url_for('static', filename='gog_logo.webp') }}"
|
||||||
alt="GOG Logo"
|
alt="GOG Logo"
|
||||||
class="game-cover"
|
class="game-cover"
|
||||||
width="368"
|
width="368"
|
||||||
height="172"
|
height="172"
|
||||||
|
@ -50,7 +41,7 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ game.name }}</td>
|
<td>{{ game.name }}</td>
|
||||||
<td class="font-monospace">{{ game.steam_key }}</td>
|
<td class="font-monospace key-col d-none d-md-table-cell">{{ game.steam_key }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if game.status == 'nicht eingelöst' %}
|
{% if game.status == 'nicht eingelöst' %}
|
||||||
<span class="badge bg-warning text-dark">{{ _('Not redeemed') }}</span>
|
<span class="badge bg-warning text-dark">{{ _('Not redeemed') }}</span>
|
||||||
|
@ -100,8 +91,8 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
{% if game.status == 'geschenkt' %}
|
{% if game.status == 'geschenkt' %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-success generate-redeem"
|
class="btn btn-sm btn-success generate-redeem"
|
||||||
data-game-id="{{ game.id }}"
|
data-game-id="{{ game.id }}"
|
||||||
title="{{ _('Generate redeem link') }}">
|
title="{{ _('Generate redeem link') }}">
|
||||||
🔗
|
🔗
|
||||||
|
@ -122,7 +113,7 @@
|
||||||
document.querySelectorAll('.generate-redeem').forEach(btn => {
|
document.querySelectorAll('.generate-redeem').forEach(btn => {
|
||||||
btn.addEventListener('click', async function() {
|
btn.addEventListener('click', async function() {
|
||||||
const gameId = this.dataset.gameId;
|
const gameId = this.dataset.gameId;
|
||||||
const flashContainer = document.querySelector('.flash-container');
|
const flashContainer = document.querySelector('.flash-container')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/generate_redeem/${gameId}`, {
|
const response = await fetch(`/generate_redeem/${gameId}`, {
|
||||||
|
@ -134,22 +125,36 @@ document.querySelectorAll('.generate-redeem').forEach(btn => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.error || '{{ _("Unknown error") }}');
|
throw new Error(data.error || '{{ _("Unknown error") }}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.url) {
|
if (data.url) {
|
||||||
await navigator.clipboard.writeText(data.url);
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(data.url)
|
||||||
// Erfolgsmeldung mit übersetztem Text
|
.then(() => {
|
||||||
flashContainer.innerHTML = `
|
// Succcess ?? maybe
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
flashContainer.innerHTML = `
|
||||||
{{ _("Link copied") }}: <a href="${data.url}" target="_blank">${data.url}</a>
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
{{ _("Link copied") }}: <a href="${data.url}" target="_blank">${data.url}</a>
|
||||||
</div>
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
`;
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
flashContainer.innerHTML = `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
{{ _("Clipboard error") }}: ${err.message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert("Clipboard API is not supported in your browser or context.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fehlermeldung mit übersetztem Text
|
// Fehlermeldung mit übersetztem Text
|
||||||
flashContainer.innerHTML = `
|
flashContainer.innerHTML = `
|
||||||
|
@ -162,7 +167,26 @@ document.querySelectorAll('.generate-redeem').forEach(btn => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log("DOM ist geladen!"); // Überprüfe, ob DOMContentLoaded überhaupt ausgeführt wird
|
||||||
|
const toggleKeysButton = document.getElementById('toggle-keys');
|
||||||
|
if (toggleKeysButton) {
|
||||||
|
console.log("Button with ID 'toggle-keys' found!");
|
||||||
|
toggleKeysButton.addEventListener('click', function() {
|
||||||
|
console.log("Button clicked!");
|
||||||
|
const keyCols = document.querySelectorAll('.key-col');
|
||||||
|
keyCols.forEach(function(el) {
|
||||||
|
el.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Button with ID 'toggle-keys' not found!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-info">{{ _('No games yet') }}</div>
|
<div class="alert alert-info">{{ _('No games yet') }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,17 @@
|
||||||
<h4>{{ _('Your Key:') }}</h4>
|
<h4>{{ _('Your Key:') }}</h4>
|
||||||
<code class="fs-3">{{ game.steam_key }}</code>
|
<code class="fs-3">{{ game.steam_key }}</code>
|
||||||
</div>
|
</div>
|
||||||
|
{% if platform_link %}
|
||||||
<a href="{{ platform_link }}{{ game.steam_key }}"
|
<a href="{{ platform_link }}{{ game.steam_key }}"
|
||||||
class="btn btn-primary btn-lg mb-3"
|
class="btn btn-primary btn-lg mb-3"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
{{ _('Redeem now on') }} {{ platform_label }}
|
{{ _('Redeem now on') }} {{ platform_name }}
|
||||||
</a>
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{{ _('Your key:') }} <code class="fs-3">{{ game.steam_key }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="mt-4 text-muted">
|
<div class="mt-4 text-muted">
|
||||||
<small>
|
<small>
|
||||||
{{ _('This page will expire in') }}
|
{{ _('This page will expire in') }}
|
||||||
|
@ -28,9 +34,9 @@
|
||||||
</small>
|
</small>
|
||||||
<div class="progress mt-2" style="height: 8px;">
|
<div class="progress mt-2" style="height: 8px;">
|
||||||
<div id="expiry-bar"
|
<div id="expiry-bar"
|
||||||
class="progress-bar bg-danger"
|
class="progress-bar bg-danger"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: 100%">
|
style="width: 100%">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"Cancel": "Abbrechen",
|
"Cancel": "Abbrechen",
|
||||||
"Change Password": "Passwort ändern",
|
"Change Password": "Passwort ändern",
|
||||||
"Change password form": "Passwort ändern Formular",
|
"Change password form": "Passwort ändern Formular",
|
||||||
|
"Clipboard error": "Ablagefehler",
|
||||||
"Confirm New Password": "Neues Passwort bestätigen",
|
"Confirm New Password": "Neues Passwort bestätigen",
|
||||||
"Confirm Password": "Passwort bestätigen",
|
"Confirm Password": "Passwort bestätigen",
|
||||||
"Copied!": "Kopiert!",
|
"Copied!": "Kopiert!",
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
"Expires at": "Läuft ab am",
|
"Expires at": "Läuft ab am",
|
||||||
"Export CSV": "CSV exportieren",
|
"Export CSV": "CSV exportieren",
|
||||||
"Externe Daten": "Externe Daten",
|
"Externe Daten": "Externe Daten",
|
||||||
|
"External Data": "Externe Daten",
|
||||||
"For GOG games: Enter the Steam AppID here to enable price tracking.": "Für GOG-Spiele: Gib hier die Steam AppID ein, um die Preisüberwachung zu aktivieren.",
|
"For GOG games: Enter the Steam AppID here to enable price tracking.": "Für GOG-Spiele: Gib hier die Steam AppID ein, um die Preisüberwachung zu aktivieren.",
|
||||||
"Game Description": "Spielbeschreibung",
|
"Game Description": "Spielbeschreibung",
|
||||||
"Game Key": "Spielschlüssel",
|
"Game Key": "Spielschlüssel",
|
||||||
|
@ -36,6 +38,7 @@
|
||||||
"Gifted": "Verschenkt",
|
"Gifted": "Verschenkt",
|
||||||
"Hist. Low": "Historischer Tiefstpreis",
|
"Hist. Low": "Historischer Tiefstpreis",
|
||||||
"Import": "Importieren",
|
"Import": "Importieren",
|
||||||
|
"Import/Export": "Import/Export",
|
||||||
"Import CSV": "CSV importieren",
|
"Import CSV": "CSV importieren",
|
||||||
"Import Games": "Spiele importieren",
|
"Import Games": "Spiele importieren",
|
||||||
"Key": "Schlüssel",
|
"Key": "Schlüssel",
|
||||||
|
@ -74,6 +77,7 @@
|
||||||
"Select CSV file": "CSV-Datei auswählen",
|
"Select CSV file": "CSV-Datei auswählen",
|
||||||
"Shop": "Shop",
|
"Shop": "Shop",
|
||||||
"Shop URL": "Shop-URL",
|
"Shop URL": "Shop-URL",
|
||||||
|
"Show/Hide Keys": "Zeige/Verstecke Keys",
|
||||||
"Sorry, you are not allowed to access this page.": "Du bist nicht berechtigt, diese Seite zu betreten.",
|
"Sorry, you are not allowed to access this page.": "Du bist nicht berechtigt, diese Seite zu betreten.",
|
||||||
"Spiel bearbeiten": "Spiel bearbeiten",
|
"Spiel bearbeiten": "Spiel bearbeiten",
|
||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
|
@ -90,5 +94,6 @@
|
||||||
"Release Date:": "Veröffentlichung:",
|
"Release Date:": "Veröffentlichung:",
|
||||||
"View Details": "Details anzeigen",
|
"View Details": "Details anzeigen",
|
||||||
"View on IsThereAnyDeal": "Auf IsThereAnyDeal ansehen",
|
"View on IsThereAnyDeal": "Auf IsThereAnyDeal ansehen",
|
||||||
"Your Key:": "Dein Schlüssel:"
|
"Your Key:": "Dein Key:",
|
||||||
}
|
"Your key:": "Dein Key"
|
||||||
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
"Action": "",
|
"Action": "",
|
||||||
"Actions": "",
|
"Actions": "",
|
||||||
"Add Game": "",
|
"Add Game": "",
|
||||||
"Add New Game": "",
|
|
||||||
"Admin": "",
|
"Admin": "",
|
||||||
"Already have an account? Login!": "",
|
"Already have an account? Login!": "",
|
||||||
"Audit Logs": "",
|
"Audit Logs": "",
|
||||||
|
@ -11,6 +10,7 @@
|
||||||
"Cancel": "",
|
"Cancel": "",
|
||||||
"Change Password": "",
|
"Change Password": "",
|
||||||
"Change password form": "",
|
"Change password form": "",
|
||||||
|
"Clipboard error": "",
|
||||||
"Confirm New Password": "",
|
"Confirm New Password": "",
|
||||||
"Confirm Password": "",
|
"Confirm Password": "",
|
||||||
"Copied!": "",
|
"Copied!": "",
|
||||||
|
@ -36,13 +36,13 @@
|
||||||
"Hist. Low": "",
|
"Hist. Low": "",
|
||||||
"Import": "",
|
"Import": "",
|
||||||
"Import CSV": "",
|
"Import CSV": "",
|
||||||
|
"Import/Export": "",
|
||||||
"Import Games": "",
|
"Import Games": "",
|
||||||
"Key": "",
|
"Key": "",
|
||||||
"Link copied": "",
|
"Link copied": "",
|
||||||
"Login": "",
|
"Login": "",
|
||||||
"Login form": "",
|
"Login form": "",
|
||||||
"Logout": "",
|
"Logout": "",
|
||||||
"My Games": "",
|
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"New Password": "",
|
"New Password": "",
|
||||||
"Next": "",
|
"Next": "",
|
||||||
|
@ -75,6 +75,7 @@
|
||||||
"Select CSV file": "",
|
"Select CSV file": "",
|
||||||
"Shop": "",
|
"Shop": "",
|
||||||
"Shop URL": "",
|
"Shop URL": "",
|
||||||
|
"Show/Hide Keys": "",
|
||||||
"Sorry, you are not allowed to access this page.": "",
|
"Sorry, you are not allowed to access this page.": "",
|
||||||
"Spiel bearbeiten": "",
|
"Spiel bearbeiten": "",
|
||||||
"Status": "",
|
"Status": "",
|
||||||
|
@ -90,5 +91,6 @@
|
||||||
"Username": "",
|
"Username": "",
|
||||||
"View Details": "",
|
"View Details": "",
|
||||||
"View on IsThereAnyDeal": "",
|
"View on IsThereAnyDeal": "",
|
||||||
|
"Your key:": "",
|
||||||
"Your Key:": ""
|
"Your Key:": ""
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue