Compare commits

..

7 Commits

7 changed files with 152 additions and 133 deletions

View File

@@ -11,6 +11,8 @@ FLASK_ENV = os.getenv("FLASK_ENV", "production")
FLASK_PORT = int(os.getenv("PORT", 5000)) FLASK_PORT = int(os.getenv("PORT", 5000))
print(f"🔧 Starting app on port {FLASK_PORT}, FLASK_ENV={FLASK_ENV}") print(f"🔧 Starting app on port {FLASK_PORT}, FLASK_ENV={FLASK_ENV}")
from flask import Flask, render_template, current_app
from markupsafe import Markup
# Initialize the CacheManager # Initialize the CacheManager
cache_manager = CacheManager() cache_manager = CacheManager()
@@ -48,6 +50,18 @@ app = Flask(__name__)
load_config(app) load_config(app)
cache_icons_and_logos(app) cache_icons_and_logos(app)
@app.context_processor
def utility_processor():
def include_svg(path):
full_path = os.path.join(current_app.root_path, 'static', path)
try:
with open(full_path, 'r', encoding='utf-8') as f:
svg = f.read()
return Markup(svg)
except IOError:
return Markup(f'<!-- SVG not found: {path} -->')
return dict(include_svg=include_svg)
@app.before_request @app.before_request
def reload_config_in_dev(): def reload_config_in_dev():
"""Reload config and recache icons before each request in development mode.""" """Reload config and recache icons before each request in development mode."""

View File

@@ -44,8 +44,8 @@ a {
} }
.card:hover { .card:hover {
background-color: var(--bs-secondary) !important; /* invert everything inside the card */
color: var(--bs-white) !important; filter: invert(0.8) hue-rotate(144deg);
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
} }
@@ -96,8 +96,11 @@ h3.footer-title {
font-size: 1.3em; font-size: 1.3em;
} }
.card-img-top i { .card-img-top i, .card-img-top svg{
font-size: 100px; font-size: 100px;
fill: currentColor;
width: 100px;
height: auto;
} }
div#navbarNavheader li.nav-item { div#navbarNavheader li.nav-item {

View File

@@ -21,71 +21,77 @@ function syncIframeHeight() {
} }
} }
// Function to open a URL in an iframe // Function to open a URL in an iframe (jQuery version mit 1500 ms Fade)
function openIframe(url) { function openIframe(url) {
enterFullscreen() enterFullscreen();
// Hide the container (and its scroll-container) so the iframe can appear in its place
if (scrollbarContainer) {
scrollbarContainer.style.display = 'none';
}
// Hide any custom scrollbar element if present var $container = scrollbarContainer ? $(scrollbarContainer) : null;
if (customScrollbar) { var $customScroll = customScrollbar ? $(customScrollbar) : null;
customScrollbar.style.display = 'none'; var $main = $(mainElement);
}
// Create or retrieve the iframe in the main element // Original-Content ausblenden mit 1500 ms
let iframe = mainElement.querySelector("iframe"); var promises = [];
if (!iframe) { if ($container) promises.push($container.fadeOut(1500).promise());
iframe = document.createElement("iframe"); if ($customScroll) promises.push($customScroll.fadeOut(1500).promise());
iframe.width = "100%";
iframe.style.border = "none";
iframe.style.overflow = "auto"; // Enable internal scrolling
iframe.scrolling = "auto";
mainElement.appendChild(iframe);
syncIframeHeight();
}
// Set the iframe's source URL $.when.apply($, promises).done(function() {
iframe.src = url; // Iframe anlegen, falls noch nicht vorhanden
var $iframe = $main.find('iframe');
if ($iframe.length === 0) {
originalMainStyle = $main.attr('style') || null;
$iframe = $('<iframe>', {
width: '100%',
frameborder: 0,
scrolling: 'auto'
}).css({ overflow: 'auto', display: 'none' });
$main.append($iframe);
}
// Push the new URL state without reloading the page // Quelle setzen und mit 1500 ms einblenden
const newUrl = new URL(window.location); $iframe
newUrl.searchParams.set('iframe', url); .attr('src', url)
window.history.pushState({ iframe: url }, '', newUrl); .fadeIn(1500, function() {
syncIframeHeight();
});
// URL-State pushen
var newUrl = new URL(window.location);
newUrl.searchParams.set('iframe', url);
window.history.pushState({ iframe: url }, '', newUrl);
});
} }
// Function to restore the original content and show the container again // Function to restore the original content (jQuery version mit 1500 ms Fade)
function restoreOriginal() { function restoreOriginal() {
// Remove the iframe from the DOM var $main = $(mainElement);
const iframe = mainElement.querySelector("iframe"); var $iframe = $main.find('iframe');
if (iframe) { var $container = scrollbarContainer ? $(scrollbarContainer) : null;
iframe.remove(); var $customScroll = customScrollbar ? $(customScrollbar) : null;
}
// Show the original container if ($iframe.length) {
if (scrollbarContainer) { // Iframe mit 1500 ms ausblenden, dann entfernen und Original einblenden
scrollbarContainer.style.display = ''; $iframe.fadeOut(1500, function() {
} $iframe.remove();
// Restore any custom scrollbar if ($container) $container.fadeIn(1500);
if (customScrollbar) { if ($customScroll) $customScroll.fadeIn(1500);
customScrollbar.style.display = '';
}
// Restore the original inline style of the main element // Inline-Style des main-Elements zurücksetzen
if (originalMainStyle !== null) { if (originalMainStyle !== null) {
mainElement.setAttribute("style", originalMainStyle); $main.attr('style', originalMainStyle);
} else { } else {
mainElement.removeAttribute("style"); $main.removeAttr('style');
} }
// Update the URL to remove the iframe parameter // URL-Parameter entfernen
const newUrl = new URL(window.location); var newUrl = new URL(window.location);
newUrl.searchParams.delete("iframe"); newUrl.searchParams.delete('iframe');
window.history.pushState({}, '', newUrl); window.history.pushState({}, '', newUrl);
});
}
} }
// Initialize event listeners after DOM content is loaded // Initialize event listeners after DOM content is loaded
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// Cache references to elements and original state // Cache references to elements and original state
@@ -105,12 +111,11 @@ document.addEventListener("DOMContentLoaded", function() {
}); });
}); });
// Clicking the header's H1 will restore the original view document.querySelectorAll(".js-restore").forEach(el => {
const headerH1 = document.querySelector("header h1"); el.style.cursor = "pointer";
if (headerH1) { el.addEventListener("click", restoreOriginal);
headerH1.style.cursor = "pointer"; });
headerH1.addEventListener("click", restoreOriginal);
}
// On full page load, check URL parameters to auto-open an iframe // On full page load, check URL parameters to auto-open an iframe
window.addEventListener("load", function() { window.addEventListener("load", function() {

View File

@@ -3,7 +3,11 @@
<head> <head>
<title>{{platform.titel}}</title> <title>{{platform.titel}}</title>
<meta charset="utf-8" > <meta charset="utf-8" >
<link rel="icon" type="image/x-icon" href="{{platform.favicon.cache}}"> <link
rel="icon"
type="image/x-icon"
href="{% if platform.favicon.cache %}{{ url_for('static', filename=platform.favicon.cache) }}{% endif %}"
>
<!-- Bootstrap CSS only --> <!-- Bootstrap CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<!-- Bootstrap JavaScript Bundle with Popper --> <!-- Bootstrap JavaScript Bundle with Popper -->
@@ -16,6 +20,11 @@
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/default.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/default.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom_scrollbar.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/custom_scrollbar.css') }}">
<!-- JQuery -->
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
crossorigin="anonymous">
</script>
</head> </head>
<body <body
{% if apod_bg %} {% if apod_bg %}
@@ -28,8 +37,11 @@
{% endif %} {% endif %}
> >
<div class="container"> <div class="container">
<header class="header"> <header class="header js-restore">
<img src="{{platform.logo.cache}}" alt="logo"/> <img
src="{{ url_for('static', filename=platform.logo.cache) }}"
alt="logo"
/>
<h1>{{platform.titel}}</h1> <h1>{{platform.titel}}</h1>
<h2>{{platform.subtitel}}</h2> <h2>{{platform.subtitel}}</h2>
</header> </header>

View File

@@ -1,28 +1,34 @@
<div class="card-column {{ lg_class }} {{ md_class }} col-12"> <div class="card-column {{ lg_class }} {{ md_class }} col-12">
<div class="card h-100 d-flex flex-column"> <div class="card h-100 d-flex flex-column">
<div class="card-body d-flex flex-column"> <div class="card-body d-flex flex-column">
<div class="card-img-top"> <div class="card-img-top">
{# Prioritize image, fallback to icon via onerror #} {% if card.icon.cache and card.icon.cache.endswith('.svg') %}
{% if card.icon.cache %} {{ include_svg(card.icon.cache) }}
<img src="{{ card.icon.cache }}" alt="{{ card.title }}" style="width:100px; height:auto;" {% elif card.icon.cache %}
onerror="this.style.display='none'; var icon=this.nextElementSibling; if(icon) icon.style.display='inline-block';"> <img
{% if card.icon.class %} src="{{ url_for('static', filename=card.icon.cache) }}"
<i class="{{ card.icon.class }}" style="display:none;"></i> alt="{{ card.title }}"
{% endif %} style="width:100px; height:auto;"
{% elif card.icon.class %} onerror="this.style.display='none'; this.nextElementSibling?.style.display='inline-block';">
<i class="{{ card.icon.class }}"></i> {% if card.icon.class %}
{% endif %} <i class="{{ card.icon.class }}" style="display:none;"></i>
</div> {% endif %}
<hr /> {% elif card.icon.class %}
<h3 class="card-title">{{ card.title }}</h3> <i class="{{ card.icon.class }}"></i>
<p class="card-text">{{ card.text }}</p> {% endif %}
{% if card.url %} </div>
<a href="{{ card.url }}" class="mt-auto btn btn-light stretched-link {% if card.iframe %}iframe-link{% endif %}"> <hr />
<i class="fa-solid fa-globe"></i> {{ card.link_text }} <h3 class="card-title">{{ card.title }}</h3>
</a> <p class="card-text">{{ card.text }}</p>
{% else %} {% if card.url %}
<i class="fa-solid fa-hourglass"></i> {{ card.link_text }} <a
{% endif %} href="{{ card.url }}"
</div> class="mt-auto btn btn-light stretched-link {% if card.iframe %}iframe-link{% endif %}">
<i class="fa-solid fa-globe"></i> {{ card.link_text }}
</a>
{% else %}
<i class="fa-solid fa-hourglass"></i> {{ card.link_text }}
{% endif %}
</div> </div>
</div>
</div> </div>

View File

@@ -54,9 +54,9 @@
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}"> <div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
{% if menu_type == "header" %} {% if menu_type == "header" %}
<a class="navbar-brand d-flex align-items-center d-none" id="navbar_logo" href="/"> <a class="navbar-brand d-flex align-items-center d-none js-restore" id="navbar_logo" href="#">
<img <img
src="{{ platform.logo.cache }}" src="{{ url_for('static', filename=platform.logo.cache) }}"
alt="{{ platform.titel }}" alt="{{ platform.titel }}"
class="d-inline-block align-text-top" class="d-inline-block align-text-top"
style="height:2rem"> style="height:2rem">

View File

@@ -1,66 +1,45 @@
import os import os
import hashlib import hashlib
import requests import requests
import mimetypes
class CacheManager: class CacheManager:
"""
A class to manage caching of files, including creating temporary directories
and caching files locally with hashed filenames.
"""
def __init__(self, cache_dir="static/cache"): def __init__(self, cache_dir="static/cache"):
"""
Initialize the CacheManager with a cache directory.
:param cache_dir: The directory where cached files will be stored.
"""
self.cache_dir = cache_dir self.cache_dir = cache_dir
self._ensure_cache_dir_exists() self._ensure_cache_dir_exists()
def _ensure_cache_dir_exists(self): def _ensure_cache_dir_exists(self):
"""
Ensure the cache directory exists. If it doesn't, create it.
"""
if not os.path.exists(self.cache_dir): if not os.path.exists(self.cache_dir):
os.makedirs(self.cache_dir) os.makedirs(self.cache_dir)
def clear_cache(self): def clear_cache(self):
"""
Clear all files in the cache directory.
"""
if os.path.exists(self.cache_dir): if os.path.exists(self.cache_dir):
for filename in os.listdir(self.cache_dir): for filename in os.listdir(self.cache_dir):
file_path = os.path.join(self.cache_dir, filename) path = os.path.join(self.cache_dir, filename)
if os.path.isfile(file_path): if os.path.isfile(path):
os.remove(file_path) os.remove(path)
def cache_file(self, file_url): def cache_file(self, file_url):
""" # generate a short hash for filename
Download a file and store it locally in the cache directory with a hashed filename. hash_suffix = hashlib.blake2s(file_url.encode('utf-8'), digest_size=8).hexdigest()
parts = file_url.rstrip("/").split("/")
base = parts[-2] if parts[-1] == "download" else parts[-1]
:param file_url: The URL of the file to cache. try:
:return: The local path of the cached file. resp = requests.get(file_url, stream=True, timeout=5)
""" resp.raise_for_status()
# Generate a hashed filename based on the URL except requests.RequestException:
hash_object = hashlib.blake2s(file_url.encode('utf-8'), digest_size=8) return None
hash_suffix = hash_object.hexdigest()
# Determine the base name for the file content_type = resp.headers.get('Content-Type', '')
splitted_file_url = file_url.split("/") ext = mimetypes.guess_extension(content_type.split(";")[0].strip()) or ".png"
base_name = splitted_file_url[-2] if splitted_file_url[-1] == "download" else splitted_file_url[-1] filename = f"{base}_{hash_suffix}{ext}"
# Construct the full path for the cached file
filename = f"{base_name}_{hash_suffix}.png"
full_path = os.path.join(self.cache_dir, filename) full_path = os.path.join(self.cache_dir, filename)
# If the file already exists, return the cached path if not os.path.exists(full_path):
if os.path.exists(full_path): with open(full_path, "wb") as f:
return full_path for chunk in resp.iter_content(1024):
f.write(chunk)
# Download the file and save it locally # return path relative to /static/
response = requests.get(file_url, stream=True) return f"cache/{filename}"
if response.status_code == 200:
with open(full_path, "wb") as file:
for chunk in response.iter_content(1024):
file.write(chunk)
return full_path