Add generate_archive.py
This commit is contained in:
parent
7aa82b7412
commit
14bf4ed086
1 changed files with 612 additions and 0 deletions
612
generate_archive.py
Normal file
612
generate_archive.py
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
import zipfile
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
def get_danish_date(timestamp):
|
||||||
|
dt = datetime.fromtimestamp(timestamp)
|
||||||
|
return format_danish_date(dt)
|
||||||
|
|
||||||
|
def format_danish_date(dt):
|
||||||
|
months_da = ['januar', 'februar', 'marts', 'april', 'maj', 'juni',
|
||||||
|
'juli', 'august', 'september', 'oktober', 'november', 'december']
|
||||||
|
return f"{dt.day}. {months_da[dt.month - 1]} {dt.year}"
|
||||||
|
|
||||||
|
def get_safe_id(mastodon_id_url):
|
||||||
|
if not mastodon_id_url:
|
||||||
|
return "unknown"
|
||||||
|
return mastodon_id_url.rstrip('/').split('/')[-1]
|
||||||
|
|
||||||
|
def generate_html_archive():
|
||||||
|
zip_filename = "archive.zip"
|
||||||
|
html_filename = "index.html"
|
||||||
|
media_dir = "media"
|
||||||
|
|
||||||
|
if not os.path.exists(zip_filename):
|
||||||
|
print(f"Fejl: Kunne ikke finde '{zip_filename}' i den nuværende mappe.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.path.exists(media_dir):
|
||||||
|
os.makedirs(media_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
zip_mtime = os.path.getmtime(zip_filename)
|
||||||
|
archive_date = get_danish_date(zip_mtime)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_filename, 'r') as archive:
|
||||||
|
zip_file_map = {os.path.basename(f): f for f in archive.namelist() if not f.endswith('/')}
|
||||||
|
|
||||||
|
# 1. Hent profiloplysninger
|
||||||
|
with archive.open('actor.json') as f:
|
||||||
|
actor = json.load(f)
|
||||||
|
|
||||||
|
# 2. Hent indlæg
|
||||||
|
with archive.open('outbox.json') as f:
|
||||||
|
outbox = json.load(f)
|
||||||
|
|
||||||
|
# 3. Udtræk profilbillede
|
||||||
|
avatar_src = ""
|
||||||
|
if 'icon' in actor and 'url' in actor['icon']:
|
||||||
|
avatar_filename_in_zip = actor['icon']['url']
|
||||||
|
try:
|
||||||
|
ext = avatar_filename_in_zip.split('.')[-1].lower()
|
||||||
|
if ext not in ['png', 'jpg', 'jpeg', 'gif']:
|
||||||
|
ext = 'png'
|
||||||
|
|
||||||
|
avatar_src = f"avatar.{ext}"
|
||||||
|
|
||||||
|
with archive.open(avatar_filename_in_zip) as f_in:
|
||||||
|
with open(avatar_src, 'wb') as f_out:
|
||||||
|
f_out.write(f_in.read())
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Udled brugerdata
|
||||||
|
display_name = actor.get('name', 'Ukendt')
|
||||||
|
username = actor.get('preferredUsername', 'ukendt')
|
||||||
|
profile_url = actor.get('url', '')
|
||||||
|
parsed_url = urlparse(profile_url)
|
||||||
|
full_handle = f"@{username}@{parsed_url.netloc}"
|
||||||
|
bio = actor.get('summary', '')
|
||||||
|
|
||||||
|
# Find profilens oprettelsesdato
|
||||||
|
profile_published = actor.get('published', '')
|
||||||
|
profile_created_str = ""
|
||||||
|
if profile_published:
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(profile_published.replace('Z', '+00:00'))
|
||||||
|
profile_created_str = f"Oprettet: {format_danish_date(dt)}"
|
||||||
|
except:
|
||||||
|
profile_created_str = f"Oprettet: {profile_published}"
|
||||||
|
|
||||||
|
# Klargør data
|
||||||
|
raw_posts = []
|
||||||
|
all_years = set()
|
||||||
|
tag_counts = {}
|
||||||
|
year_counts = {}
|
||||||
|
|
||||||
|
# Tællere til indhold og typer
|
||||||
|
content_counts = {'media': 0, 'links': 0, 'tags': 0, 'no_tags': 0}
|
||||||
|
|
||||||
|
ordered_items = outbox.get('orderedItems', [])
|
||||||
|
for item in ordered_items:
|
||||||
|
if item.get('type') == 'Create':
|
||||||
|
obj = item.get('object', {})
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
raw_posts.append(obj)
|
||||||
|
|
||||||
|
posts = []
|
||||||
|
post_count = 0
|
||||||
|
reply_count = 0
|
||||||
|
quote_count = 0
|
||||||
|
|
||||||
|
for obj in raw_posts:
|
||||||
|
obj_id = obj.get('id', '')
|
||||||
|
safe_id = get_safe_id(obj_id)
|
||||||
|
raw_content = obj.get('content', '')
|
||||||
|
|
||||||
|
clean_text = re.sub(r'<[^>]+>', '', raw_content).strip()
|
||||||
|
is_quote = clean_text.startswith('RE:')
|
||||||
|
is_reply = obj.get('inReplyTo') is not None
|
||||||
|
|
||||||
|
if is_quote:
|
||||||
|
quote_count += 1
|
||||||
|
post_type_label = "Citat"
|
||||||
|
post_type_class = "citat"
|
||||||
|
elif is_reply:
|
||||||
|
reply_count += 1
|
||||||
|
post_type_label = "Svar"
|
||||||
|
post_type_class = "svar"
|
||||||
|
else:
|
||||||
|
post_count += 1
|
||||||
|
post_type_label = "Post"
|
||||||
|
post_type_class = "post"
|
||||||
|
|
||||||
|
# Parse dato
|
||||||
|
published_str = obj.get('published', '')
|
||||||
|
post_year = "Ukendt"
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(published_str.replace('Z', '+00:00'))
|
||||||
|
formatted_date = dt.strftime('%d. %b %Y kl. %H:%M')
|
||||||
|
post_year = str(dt.year)
|
||||||
|
all_years.add(post_year)
|
||||||
|
year_counts[post_year] = year_counts.get(post_year, 0) + 1
|
||||||
|
except:
|
||||||
|
formatted_date = published_str
|
||||||
|
|
||||||
|
# Parse tags
|
||||||
|
post_tags = []
|
||||||
|
tags_data = obj.get('tag', [])
|
||||||
|
for t in tags_data:
|
||||||
|
if isinstance(t, dict) and t.get('type') == 'Hashtag':
|
||||||
|
tag_name = t.get('name', '').lower()
|
||||||
|
if tag_name:
|
||||||
|
post_tags.append(tag_name)
|
||||||
|
tag_counts[tag_name] = tag_counts.get(tag_name, 0) + 1
|
||||||
|
tags_attr = ",".join(post_tags)
|
||||||
|
|
||||||
|
# Tjek om der er eksterne links
|
||||||
|
has_links_bool = "false"
|
||||||
|
a_tags = re.findall(r'<a\s+[^>]*>', raw_content)
|
||||||
|
for a in a_tags:
|
||||||
|
class_match = re.search(r'class="([^"]*)"', a)
|
||||||
|
if class_match:
|
||||||
|
classes = class_match.group(1).split()
|
||||||
|
if 'mention' not in classes and 'hashtag' not in classes:
|
||||||
|
has_links_bool = "true"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
has_links_bool = "true"
|
||||||
|
break
|
||||||
|
|
||||||
|
# Håndter medier
|
||||||
|
media_html = ""
|
||||||
|
attachments = obj.get('attachment', [])
|
||||||
|
for att in attachments:
|
||||||
|
att_url = att.get('url', '')
|
||||||
|
if att_url:
|
||||||
|
filename = os.path.basename(urlparse(att_url).path)
|
||||||
|
if filename in zip_file_map:
|
||||||
|
zip_path = zip_file_map[filename]
|
||||||
|
local_media_path = os.path.join(media_dir, filename)
|
||||||
|
try:
|
||||||
|
with archive.open(zip_path) as f_in, open(local_media_path, 'wb') as f_out:
|
||||||
|
f_out.write(f_in.read())
|
||||||
|
|
||||||
|
media_type = att.get('mediaType', '')
|
||||||
|
if media_type.startswith('image/'):
|
||||||
|
media_html += f'<img src="{media_dir}/{filename}" alt="Vedhæftet billede" class="post-media-item">'
|
||||||
|
elif media_type.startswith('video/'):
|
||||||
|
media_html += f'<video controls src="{media_dir}/{filename}" class="post-media-item"></video>'
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Kunne ikke udpakke medie {filename}: {e}")
|
||||||
|
|
||||||
|
if media_html:
|
||||||
|
media_html = f'<div class="post-media-container">{media_html}</div>'
|
||||||
|
|
||||||
|
# Sæt boolske attributter til indholdsfiltrering
|
||||||
|
has_media_bool = "true" if media_html else "false"
|
||||||
|
has_tags_bool = "true" if post_tags else "false"
|
||||||
|
|
||||||
|
# Opdater tællere for indhold
|
||||||
|
if has_media_bool == "true": content_counts['media'] += 1
|
||||||
|
if has_links_bool == "true": content_counts['links'] += 1
|
||||||
|
if has_tags_bool == "true": content_counts['tags'] += 1
|
||||||
|
else: content_counts['no_tags'] += 1
|
||||||
|
|
||||||
|
posts.append({
|
||||||
|
'safe_id': safe_id,
|
||||||
|
'content': raw_content,
|
||||||
|
'media_html': media_html,
|
||||||
|
'date': formatted_date,
|
||||||
|
'url': obj.get('url', profile_url),
|
||||||
|
'raw_date': published_str,
|
||||||
|
'year': post_year,
|
||||||
|
'tags_attr': tags_attr,
|
||||||
|
'type_label': post_type_label,
|
||||||
|
'type_class': post_type_class,
|
||||||
|
'has_media': has_media_bool,
|
||||||
|
'has_links': has_links_bool,
|
||||||
|
'has_tags': has_tags_bool
|
||||||
|
})
|
||||||
|
|
||||||
|
posts.sort(key=lambda x: x['raw_date'], reverse=True)
|
||||||
|
sorted_years = sorted(list(all_years), reverse=True)
|
||||||
|
sorted_tags = sorted(tag_counts.items(), key=lambda item: (-item[1], item[0]))
|
||||||
|
|
||||||
|
total_posts = len(posts)
|
||||||
|
|
||||||
|
# Byg HTML til år-knapper
|
||||||
|
years_html = f'<button class="filter-btn active year-btn" data-year="all" onclick="setYearFilter(\'all\', this)">Alle år ({total_posts})</button>\n'
|
||||||
|
for year in sorted_years:
|
||||||
|
if year != "Ukendt":
|
||||||
|
count = year_counts.get(year, 0)
|
||||||
|
years_html += f'<button class="filter-btn year-btn" data-year="{year}" onclick="setYearFilter(\'{year}\', this)">{year} ({count})</button>\n'
|
||||||
|
|
||||||
|
# Byg HTML til tag-knapper
|
||||||
|
tags_html = ""
|
||||||
|
if sorted_tags:
|
||||||
|
tags_html += '<div class="filter-row">'
|
||||||
|
tags_html += '<span class="filter-label">Tags:</span>'
|
||||||
|
tags_html += '<div class="filter-buttons">'
|
||||||
|
tags_html += f'<button class="filter-btn active tag-btn" data-tag="all" onclick="setTagFilter(\'all\', this)">Alle tags ({total_posts})</button>\n'
|
||||||
|
for tag, count in sorted_tags:
|
||||||
|
display_tag = tag if tag.startswith('#') else f"#{tag}"
|
||||||
|
tags_html += f'<button class="filter-btn tag-btn" data-tag="{tag}" onclick="setTagFilter(\'{tag}\', this)">{display_tag} ({count})</button>\n'
|
||||||
|
tags_html += '</div></div>'
|
||||||
|
|
||||||
|
# --- Byg HTML ---
|
||||||
|
html_content = f"""<!DOCTYPE html>
|
||||||
|
<html lang="da">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Mastodon Arkiv - {display_name}</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
--primary: #6364ff;
|
||||||
|
--primary-hover: #563acc;
|
||||||
|
--bg: #f5f8fa;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--text-main: #282c37;
|
||||||
|
--text-muted: #9baec8;
|
||||||
|
--border: #d9e1e8;
|
||||||
|
}}
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: var(--bg); color: var(--text-main); line-height: 1.6;
|
||||||
|
margin: 0; padding: 20px; scroll-behavior: smooth;
|
||||||
|
}}
|
||||||
|
.page-container {{ max-width: 1100px; margin: 0 auto; }}
|
||||||
|
|
||||||
|
.mastodon-title-container {{
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
gap: 15px; margin-bottom: 30px;
|
||||||
|
}}
|
||||||
|
.mastodon-title-container h1 {{ color: var(--text-main); margin: 0; font-size: 2.5em; }}
|
||||||
|
.mastodon-logo {{ width: 45px; height: 45px; color: var(--primary); }}
|
||||||
|
|
||||||
|
.layout-wrapper {{ display: flex; gap: 30px; align-items: flex-start; }}
|
||||||
|
.main-content {{ flex: 1; min-width: 0; }}
|
||||||
|
.sidebar {{
|
||||||
|
width: 320px; flex-shrink: 0; position: sticky;
|
||||||
|
top: 20px; transition: top 0.1s;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.profile-card {{
|
||||||
|
background: var(--card-bg); border-radius: 10px; padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px;
|
||||||
|
display: flex; gap: 20px; align-items: flex-start;
|
||||||
|
}}
|
||||||
|
.profile-pic {{
|
||||||
|
width: 100px; height: 100px; border-radius: 50%;
|
||||||
|
object-fit: cover; background-color: var(--border); flex-shrink: 0;
|
||||||
|
}}
|
||||||
|
.profile-info {{ flex: 1; display: flex; flex-direction: column; justify-content: center; }}
|
||||||
|
.profile-info h2 {{ margin: 0 0 5px 0; font-size: 1.5em; }}
|
||||||
|
.profile-handle {{ color: var(--text-muted); font-weight: bold; margin-bottom: 5px; }}
|
||||||
|
.profile-created {{ font-size: 0.85em; color: var(--text-muted); margin-bottom: 10px; }}
|
||||||
|
.profile-bio {{ margin-bottom: 10px; }}
|
||||||
|
.profile-stats {{ display: flex; gap: 15px; font-size: 0.9em; flex-wrap: wrap; }}
|
||||||
|
.profile-stats span {{ background: var(--bg); padding: 5px 10px; border-radius: 5px; border: 1px solid var(--border); }}
|
||||||
|
|
||||||
|
.moved-notice {{
|
||||||
|
background-color: #f0efff; border-left: 4px solid var(--primary);
|
||||||
|
padding: 20px; border-radius: 4px; margin-bottom: 30px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
}}
|
||||||
|
.moved-notice p {{ margin: 0 0 10px 0; }}
|
||||||
|
.moved-notice a {{ color: var(--primary); font-weight: bold; text-decoration: none; }}
|
||||||
|
.moved-notice a:hover {{ text-decoration: underline; }}
|
||||||
|
.archive-date {{ font-size: 0.85em; color: #828ca0; margin-top: 15px !important; border-top: 1px solid #dcdbe8; padding-top: 10px; }}
|
||||||
|
|
||||||
|
.controls-container {{
|
||||||
|
background: var(--card-bg); padding: 20px; border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
display: flex; flex-direction: column; gap: 20px;
|
||||||
|
}}
|
||||||
|
.search-container {{ display: flex; flex-direction: column; gap: 8px; }}
|
||||||
|
.search-container label {{ font-weight: bold; font-size: 0.9em; color: var(--text-muted); }}
|
||||||
|
.search-input-group {{ display: flex; gap: 8px; }}
|
||||||
|
#searchInput {{
|
||||||
|
flex-grow: 1; padding: 10px; border: 1px solid var(--border);
|
||||||
|
border-radius: 5px; font-size: 1em; box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
#searchInput:focus {{ outline: none; border-color: var(--primary); }}
|
||||||
|
#searchBtn {{
|
||||||
|
padding: 0 15px; background-color: var(--primary); color: white;
|
||||||
|
border: none; border-radius: 5px; cursor: pointer; font-weight: bold;
|
||||||
|
transition: background-color 0.2s; font-size: 0.9em;
|
||||||
|
}}
|
||||||
|
#searchBtn:hover {{ background-color: var(--primary-hover); }}
|
||||||
|
|
||||||
|
.result-count {{ font-size: 0.85em; color: var(--text-muted); font-weight: 500; }}
|
||||||
|
|
||||||
|
.filter-row {{ display: flex; flex-direction: column; gap: 8px; }}
|
||||||
|
.filter-label {{ font-weight: bold; font-size: 0.9em; color: var(--text-muted); }}
|
||||||
|
.filter-buttons {{ display: flex; gap: 6px; flex-wrap: wrap; }}
|
||||||
|
.filter-btn {{
|
||||||
|
padding: 6px 12px; background-color: var(--bg); color: var(--text-main);
|
||||||
|
border: 1px solid var(--border); border-radius: 5px; cursor: pointer;
|
||||||
|
font-size: 0.9em; transition: all 0.2s;
|
||||||
|
}}
|
||||||
|
.filter-btn.active {{ background-color: var(--primary); color: white; border-color: var(--primary); }}
|
||||||
|
.filter-btn:hover:not(.active) {{ background-color: #e2eaf0; }}
|
||||||
|
|
||||||
|
.sidebar-footer {{
|
||||||
|
margin-top: 10px; padding-top: 15px; border-top: 1px solid var(--border);
|
||||||
|
text-align: center; font-size: 0.85em; color: var(--text-muted);
|
||||||
|
}}
|
||||||
|
.sidebar-footer a {{ color: var(--primary); text-decoration: none; font-weight: 600; }}
|
||||||
|
.sidebar-footer a:hover {{ text-decoration: underline; }}
|
||||||
|
|
||||||
|
.mastodon-post {{
|
||||||
|
background: var(--card-bg); border-radius: 10px; padding: 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 15px; scroll-margin-top: 20px;
|
||||||
|
}}
|
||||||
|
.post-header {{ display: flex; align-items: center; gap: 10px; margin-bottom: 15px; }}
|
||||||
|
.post-mini-pic {{ width: 40px; height: 40px; border-radius: 50%; object-fit: cover; }}
|
||||||
|
.post-author {{ font-weight: bold; }}
|
||||||
|
.post-content {{ margin-bottom: 15px; word-break: break-word; }}
|
||||||
|
.post-content p {{ margin-top: 0; }}
|
||||||
|
|
||||||
|
.post-media-container {{ margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 10px; }}
|
||||||
|
.post-media-item {{ max-width: 100%; max-height: 500px; border-radius: 8px; border: 1px solid var(--border); }}
|
||||||
|
|
||||||
|
.post-meta {{
|
||||||
|
font-size: 0.85em; color: var(--text-muted); display: flex;
|
||||||
|
justify-content: space-between; align-items: center;
|
||||||
|
border-top: 1px solid var(--border); padding-top: 10px;
|
||||||
|
}}
|
||||||
|
.post-meta-right {{ display: flex; align-items: center; gap: 15px; flex-wrap: wrap; justify-content: flex-end; }}
|
||||||
|
.type-badge {{ background: var(--bg); padding: 3px 8px; border-radius: 4px; border: 1px solid var(--border); font-weight: bold; color: var(--text-main); }}
|
||||||
|
.type-badge.Svar {{ background-color: #f0efff; color: var(--primary-hover); }}
|
||||||
|
.type-badge.Citat {{ background-color: #fff4e5; color: #d97706; border-color: #fde68a; }}
|
||||||
|
.post-meta a {{ color: var(--primary); text-decoration: none; }}
|
||||||
|
.post-meta a:hover {{ text-decoration: underline; }}
|
||||||
|
|
||||||
|
@media (max-width: 850px) {{
|
||||||
|
.layout-wrapper {{ flex-direction: column; }}
|
||||||
|
.sidebar {{ width: 100%; position: static; order: -1; }}
|
||||||
|
.profile-card {{ flex-direction: column; align-items: center; text-align: center; }}
|
||||||
|
.profile-stats {{ justify-content: center; }}
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="page-container">
|
||||||
|
|
||||||
|
<div class="mastodon-title-container">
|
||||||
|
<svg class="mastodon-logo" viewBox="0 0 216.4144 232.00976" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="currentColor" d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915"/>
|
||||||
|
<path fill="#fff" d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.5225 15.17125-11.28875 25.80625-11.28875 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.635 0 19.2375 3.76625 25.80625 11.28875 6.36875 7.3225 9.54 17.22 9.54 29.675"/>
|
||||||
|
</svg>
|
||||||
|
<h1>Mastodon Arkiv</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layout-wrapper">
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="profile-card">
|
||||||
|
<img src="{avatar_src}" alt="Profilbillede" class="profile-pic">
|
||||||
|
<div class="profile-info">
|
||||||
|
<h2>{display_name}</h2>
|
||||||
|
<div class="profile-handle">{full_handle}</div>
|
||||||
|
{f'<div class="profile-created">{profile_created_str}</div>' if profile_created_str else ''}
|
||||||
|
<div class="profile-bio">{bio}</div>
|
||||||
|
<div class="profile-stats">
|
||||||
|
<span><strong>{post_count}</strong> Posts</span>
|
||||||
|
<span><strong>{reply_count}</strong> Svar</span>
|
||||||
|
<span><strong>{quote_count}</strong> Citater</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="moved-notice">
|
||||||
|
<p><strong>Kontoen er flyttet!</strong></p>
|
||||||
|
<p>Min nye Mastodon instans og profil kan findes her: <a href="https://andersen.one/@aphandersen" target="_blank">@aphandersen@andersen.one</a></p>
|
||||||
|
<p class="archive-date">Dette historiske Mastodon-arkiv blev genereret den {archive_date}.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="postsList">
|
||||||
|
"""
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
html_content += f"""
|
||||||
|
<div class="mastodon-post"
|
||||||
|
data-type="{post['type_class']}"
|
||||||
|
data-year="{post['year']}"
|
||||||
|
data-tags="{post['tags_attr']}"
|
||||||
|
data-has-media="{post['has_media']}"
|
||||||
|
data-has-links="{post['has_links']}"
|
||||||
|
data-has-tags="{post['has_tags']}"
|
||||||
|
id="post-{post['safe_id']}">
|
||||||
|
<div class="post-header">
|
||||||
|
<img src="{avatar_src}" alt="Mini profilbillede" class="post-mini-pic">
|
||||||
|
<div class="post-author">{display_name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-content">
|
||||||
|
{post['content']}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{post['media_html']}
|
||||||
|
|
||||||
|
<div class="post-meta">
|
||||||
|
<span class="post-date">{post['date']}</span>
|
||||||
|
<div class="post-meta-right">
|
||||||
|
<span class="type-badge {post['type_label']}">{post['type_label']}</span>
|
||||||
|
<a href="{post['url']}" target="_blank">Original URL ↗</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
html_content += f"""
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<aside class="sidebar" id="filterSidebar">
|
||||||
|
<div class="controls-container">
|
||||||
|
<div class="search-container">
|
||||||
|
<label for="searchInput">Søg:</label>
|
||||||
|
<div class="search-input-group">
|
||||||
|
<input type="text" id="searchInput" placeholder="Søg i indlæg...">
|
||||||
|
<button id="searchBtn" onclick="applyFilters()">Søg</button>
|
||||||
|
</div>
|
||||||
|
<div id="resultCount" class="result-count"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-label">Type:</span>
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<button class="filter-btn active type-btn" data-filter="all" onclick="setTypeFilter('all', this)">Vis alle ({total_posts})</button>
|
||||||
|
<button class="filter-btn type-btn" data-filter="post" onclick="setTypeFilter('post', this)">Kun Posts ({post_count})</button>
|
||||||
|
<button class="filter-btn type-btn" data-filter="svar" onclick="setTypeFilter('svar', this)">Kun Svar ({reply_count})</button>
|
||||||
|
<button class="filter-btn type-btn" data-filter="citat" onclick="setTypeFilter('citat', this)">Kun Citater ({quote_count})</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-label">Indhold:</span>
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<button class="filter-btn active content-btn" data-content="all" onclick="setContentFilter('all', this)">Alt ({total_posts})</button>
|
||||||
|
<button class="filter-btn content-btn" data-content="media" onclick="setContentFilter('media', this)">Med billeder ({content_counts['media']})</button>
|
||||||
|
<button class="filter-btn content-btn" data-content="links" onclick="setContentFilter('links', this)">Med links ({content_counts['links']})</button>
|
||||||
|
<button class="filter-btn content-btn" data-content="tags" onclick="setContentFilter('tags', this)">Med tags ({content_counts['tags']})</button>
|
||||||
|
<button class="filter-btn content-btn" data-content="no_tags" onclick="setContentFilter('no_tags', this)">Uden tags ({content_counts['no_tags']})</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-label">År:</span>
|
||||||
|
<div class="filter-buttons">
|
||||||
|
{years_html}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tags_html}
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
© <a href="https://aandersen.eu" target="_blank">Andreas Andersen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentFilterType = 'all';
|
||||||
|
let currentFilterContent = 'all';
|
||||||
|
let currentFilterYear = 'all';
|
||||||
|
let currentFilterTag = 'all';
|
||||||
|
|
||||||
|
// Gør så Enter-tasten i søgefeltet også udfører en søgning
|
||||||
|
document.getElementById("searchInput").addEventListener("keyup", function(event) {{
|
||||||
|
if (event.key === "Enter") {{
|
||||||
|
applyFilters();
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
function setTypeFilter(type, btnElement) {{
|
||||||
|
currentFilterType = type;
|
||||||
|
document.querySelectorAll('.type-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
btnElement.classList.add('active');
|
||||||
|
applyFilters();
|
||||||
|
}}
|
||||||
|
|
||||||
|
function setContentFilter(content, btnElement) {{
|
||||||
|
currentFilterContent = content;
|
||||||
|
document.querySelectorAll('.content-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
btnElement.classList.add('active');
|
||||||
|
applyFilters();
|
||||||
|
}}
|
||||||
|
|
||||||
|
function setYearFilter(year, btnElement) {{
|
||||||
|
currentFilterYear = year;
|
||||||
|
document.querySelectorAll('.year-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
btnElement.classList.add('active');
|
||||||
|
applyFilters();
|
||||||
|
}}
|
||||||
|
|
||||||
|
function setTagFilter(tag, btnElement) {{
|
||||||
|
currentFilterTag = tag;
|
||||||
|
document.querySelectorAll('.tag-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
btnElement.classList.add('active');
|
||||||
|
applyFilters();
|
||||||
|
}}
|
||||||
|
|
||||||
|
function applyFilters() {{
|
||||||
|
const query = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
const posts = document.querySelectorAll('.mastodon-post');
|
||||||
|
let visibleCount = 0;
|
||||||
|
const totalCount = posts.length;
|
||||||
|
|
||||||
|
posts.forEach(post => {{
|
||||||
|
const content = post.querySelector('.post-content').innerText.toLowerCase();
|
||||||
|
const postType = post.getAttribute('data-type');
|
||||||
|
const postYear = post.getAttribute('data-year');
|
||||||
|
const postTags = post.getAttribute('data-tags') ? post.getAttribute('data-tags').split(',') : [];
|
||||||
|
const hasMedia = post.getAttribute('data-has-media') === 'true';
|
||||||
|
const hasLinks = post.getAttribute('data-has-links') === 'true';
|
||||||
|
const hasTagsBool = post.getAttribute('data-has-tags') === 'true';
|
||||||
|
|
||||||
|
const matchesSearch = content.includes(query);
|
||||||
|
const matchesType = (currentFilterType === 'all') || (currentFilterType === postType);
|
||||||
|
const matchesYear = (currentFilterYear === 'all') || (currentFilterYear === postYear);
|
||||||
|
const matchesTag = (currentFilterTag === 'all') || postTags.includes(currentFilterTag);
|
||||||
|
|
||||||
|
let matchesContent = true;
|
||||||
|
if (currentFilterContent === 'media') matchesContent = hasMedia;
|
||||||
|
if (currentFilterContent === 'links') matchesContent = hasLinks;
|
||||||
|
if (currentFilterContent === 'tags') matchesContent = hasTagsBool;
|
||||||
|
if (currentFilterContent === 'no_tags') matchesContent = !hasTagsBool;
|
||||||
|
|
||||||
|
if (matchesSearch && matchesType && matchesContent && matchesYear && matchesTag) {{
|
||||||
|
post.style.display = 'block';
|
||||||
|
visibleCount++;
|
||||||
|
}} else {{
|
||||||
|
post.style.display = 'none';
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
document.getElementById('resultCount').innerText = `Viser ${{visibleCount}} af ${{totalCount}} indlæg`;
|
||||||
|
setTimeout(updateSidebarSticky, 50);
|
||||||
|
}}
|
||||||
|
|
||||||
|
function updateSidebarSticky() {{
|
||||||
|
const sidebar = document.getElementById('filterSidebar');
|
||||||
|
if (!sidebar || window.innerWidth <= 850) return;
|
||||||
|
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const sidebarHeight = sidebar.offsetHeight;
|
||||||
|
|
||||||
|
if (sidebarHeight > windowHeight - 40) {{
|
||||||
|
const offset = windowHeight - sidebarHeight - 20;
|
||||||
|
sidebar.style.top = offset + 'px';
|
||||||
|
}} else {{
|
||||||
|
sidebar.style.top = '20px';
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {{
|
||||||
|
applyFilters();
|
||||||
|
updateSidebarSticky();
|
||||||
|
}});
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateSidebarSticky);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
with open(html_filename, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
print(f"Succes! {html_filename} er nu blevet genereret.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Der opstod en fejl under behandlingen af arkivet: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
generate_html_archive()
|
||||||
Loading…
Reference in a new issue