mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2025-09-09 19:27:11 +02:00
Compare commits
7 Commits
246ef1b059
...
b0446dcd29
Author | SHA1 | Date | |
---|---|---|---|
b0446dcd29 | |||
55d309b2d7 | |||
d99a8c8452 | |||
3f6a195ecb | |||
430ea4a120 | |||
cc0fc9b77f | |||
9ada9acb3a |
14
app/app.py
14
app/app.py
@@ -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."""
|
||||
|
@@ -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 {
|
||||
|
@@ -21,70 +21,76 @@ 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();
|
||||
|
||||
var $container = scrollbarContainer ? $(scrollbarContainer) : null;
|
||||
var $customScroll = customScrollbar ? $(customScrollbar) : null;
|
||||
var $main = $(mainElement);
|
||||
|
||||
// Original-Content ausblenden mit 1500 ms
|
||||
var promises = [];
|
||||
if ($container) promises.push($container.fadeOut(1500).promise());
|
||||
if ($customScroll) promises.push($customScroll.fadeOut(1500).promise());
|
||||
|
||||
$.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);
|
||||
}
|
||||
|
||||
// Hide any custom scrollbar element if present
|
||||
if (customScrollbar) {
|
||||
customScrollbar.style.display = 'none';
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Quelle setzen und mit 1500 ms einblenden
|
||||
$iframe
|
||||
.attr('src', url)
|
||||
.fadeIn(1500, function() {
|
||||
syncIframeHeight();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the iframe's source URL
|
||||
iframe.src = url;
|
||||
|
||||
// Push the new URL state without reloading the page
|
||||
const newUrl = new URL(window.location);
|
||||
// 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
|
||||
// Inline-Style des main-Elements zurücksetzen
|
||||
if (originalMainStyle !== null) {
|
||||
mainElement.setAttribute("style", originalMainStyle);
|
||||
$main.attr('style', originalMainStyle);
|
||||
} else {
|
||||
mainElement.removeAttribute("style");
|
||||
$main.removeAttr('style');
|
||||
}
|
||||
|
||||
// Update the URL to remove the iframe parameter
|
||||
const newUrl = new URL(window.location);
|
||||
newUrl.searchParams.delete("iframe");
|
||||
// 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() {
|
||||
@@ -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() {
|
||||
|
@@ -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>
|
||||
|
@@ -2,10 +2,14 @@
|
||||
<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.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 %}
|
||||
@@ -17,7 +21,9 @@
|
||||
<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 %}">
|
||||
<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 %}
|
||||
|
@@ -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">
|
||||
|
@@ -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}"
|
||||
|
Reference in New Issue
Block a user