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))
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
cache_manager = CacheManager()
@@ -48,6 +50,18 @@ app = Flask(__name__)
load_config(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
def reload_config_in_dev():
"""Reload config and recache icons before each request in development mode."""

View File

@@ -44,8 +44,8 @@ a {
}
.card:hover {
background-color: var(--bs-secondary) !important;
color: var(--bs-white) !important;
/* invert everything inside the card */
filter: invert(0.8) hue-rotate(144deg);
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
@@ -96,8 +96,11 @@ h3.footer-title {
font-size: 1.3em;
}
.card-img-top i {
.card-img-top i, .card-img-top svg{
font-size: 100px;
fill: currentColor;
width: 100px;
height: auto;
}
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) {
enterFullscreen()
// Hide the container (and its scroll-container) so the iframe can appear in its place
if (scrollbarContainer) {
scrollbarContainer.style.display = 'none';
}
enterFullscreen();
// Hide any custom scrollbar element if present
if (customScrollbar) {
customScrollbar.style.display = 'none';
}
var $container = scrollbarContainer ? $(scrollbarContainer) : null;
var $customScroll = customScrollbar ? $(customScrollbar) : null;
var $main = $(mainElement);
// Create or retrieve the iframe in the main element
let iframe = mainElement.querySelector("iframe");
if (!iframe) {
iframe = document.createElement("iframe");
iframe.width = "100%";
iframe.style.border = "none";
iframe.style.overflow = "auto"; // Enable internal scrolling
iframe.scrolling = "auto";
mainElement.appendChild(iframe);
syncIframeHeight();
}
// Original-Content ausblenden mit 1500 ms
var promises = [];
if ($container) promises.push($container.fadeOut(1500).promise());
if ($customScroll) promises.push($customScroll.fadeOut(1500).promise());
// Set the iframe's source URL
iframe.src = url;
$.when.apply($, promises).done(function() {
// 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
const newUrl = new URL(window.location);
newUrl.searchParams.set('iframe', url);
window.history.pushState({ iframe: url }, '', newUrl);
// Quelle setzen und mit 1500 ms einblenden
$iframe
.attr('src', url)
.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() {
// Remove the iframe from the DOM
const iframe = mainElement.querySelector("iframe");
if (iframe) {
iframe.remove();
}
var $main = $(mainElement);
var $iframe = $main.find('iframe');
var $container = scrollbarContainer ? $(scrollbarContainer) : null;
var $customScroll = customScrollbar ? $(customScrollbar) : null;
// Show the original container
if (scrollbarContainer) {
scrollbarContainer.style.display = '';
}
if ($iframe.length) {
// Iframe mit 1500 ms ausblenden, dann entfernen und Original einblenden
$iframe.fadeOut(1500, function() {
$iframe.remove();
// Restore any custom scrollbar
if (customScrollbar) {
customScrollbar.style.display = '';
}
if ($container) $container.fadeIn(1500);
if ($customScroll) $customScroll.fadeIn(1500);
// Restore the original inline style of the main element
if (originalMainStyle !== null) {
mainElement.setAttribute("style", originalMainStyle);
} else {
mainElement.removeAttribute("style");
}
// Inline-Style des main-Elements zurücksetzen
if (originalMainStyle !== null) {
$main.attr('style', originalMainStyle);
} else {
$main.removeAttr('style');
}
// Update the URL to remove the iframe parameter
const newUrl = new URL(window.location);
newUrl.searchParams.delete("iframe");
window.history.pushState({}, '', newUrl);
// URL-Parameter entfernen
var newUrl = new URL(window.location);
newUrl.searchParams.delete('iframe');
window.history.pushState({}, '', newUrl);
});
}
}
// Initialize event listeners after DOM content is loaded
document.addEventListener("DOMContentLoaded", function() {
// 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
const headerH1 = document.querySelector("header h1");
if (headerH1) {
headerH1.style.cursor = "pointer";
headerH1.addEventListener("click", restoreOriginal);
}
document.querySelectorAll(".js-restore").forEach(el => {
el.style.cursor = "pointer";
el.addEventListener("click", restoreOriginal);
});
// On full page load, check URL parameters to auto-open an iframe
window.addEventListener("load", function() {

View File

@@ -3,7 +3,11 @@
<head>
<title>{{platform.titel}}</title>
<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 -->
<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 -->
@@ -16,6 +20,11 @@
<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/custom_scrollbar.css') }}">
<!-- JQuery -->
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
crossorigin="anonymous">
</script>
</head>
<body
{% if apod_bg %}
@@ -28,8 +37,11 @@
{% endif %}
>
<div class="container">
<header class="header">
<img src="{{platform.logo.cache}}" alt="logo"/>
<header class="header js-restore">
<img
src="{{ url_for('static', filename=platform.logo.cache) }}"
alt="logo"
/>
<h1>{{platform.titel}}</h1>
<h2>{{platform.subtitel}}</h2>
</header>

View File

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

View File

@@ -54,9 +54,9 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav{{menu_type}}">
{% 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
src="{{ platform.logo.cache }}"
src="{{ url_for('static', filename=platform.logo.cache) }}"
alt="{{ platform.titel }}"
class="d-inline-block align-text-top"
style="height:2rem">

View File

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