add fedipage datamodel

This commit is contained in:
2025-09-21 19:07:33 +02:00
parent c679d9fa3b
commit 864a1f4aef
7 changed files with 781 additions and 191 deletions
@@ -0,0 +1,51 @@
package de.jalin.fedipage;
import java.io.Serializable;
public class FediMedium implements Serializable {
private static final long serialVersionUID = 2056053678422715807L;
public static final String IMAGE = "image";
public static final String VIDEO = "video";
public static final String AUDIO = "audio";
public static final String CARD = "card";
private String mediumType;
private String description;
private String mediumUrl;
private String mediumImage;
public String getMediumType() {
return mediumType;
}
public void setMediumType(String mediumType) {
this.mediumType = mediumType;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getMediumUrl() {
return mediumUrl;
}
public void setMediumUrl(String mediumUrl) {
this.mediumUrl = mediumUrl;
}
public String getMediumImage() {
return mediumImage;
}
public void setMediumImage(String mediumImage) {
this.mediumImage = mediumImage;
}
}
@@ -0,0 +1,122 @@
package de.jalin.fedipage;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FediPage implements Serializable {
private static final long serialVersionUID = 621351254124767964L;
private final String profileName;
private final String instance;
private final List<FediPost> timeline;
private String displayName;
private String description;
private String url;
private String avatarUrl;
private String headerUrl;
private int followers;
private int following;
private int posts;
public FediPage(final String handle, final String domain) {
this.profileName = handle;
this.instance = domain;
this.setDisplayName("Nobody");
this.setDescription("no text");
this.setUrl("undef");
this.setAvatarUrl("undef");
this.setHeaderUrl("undef");
this.setFollowers(0);
this.setFollowing(0);
this.setPosts(0);
this.timeline = new ArrayList<FediPost>();
}
public String getUsername() {
return "@" + getProfileName() + "@" + getInstance();
}
public String getProfileName() {
return profileName;
}
public String getInstance() {
return instance;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getFollowers() {
return followers;
}
public void setFollowers(int followers) {
this.followers = followers;
}
public int getFollowing() {
return following;
}
public void setFollowing(int following) {
this.following = following;
}
public int getPosts() {
return posts;
}
public void setPosts(int posts) {
this.posts = posts;
}
public void setUrl(String profileUrl) {
this.url = profileUrl;
}
public String getUrl() {
return url;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getHeaderUrl() {
return headerUrl;
}
public void setHeaderUrl(String headerUrl) {
this.headerUrl = headerUrl;
}
public void append(final FediPost post) {
timeline.add(post);
}
public List<FediPost> getTimeline() {
return Collections.unmodifiableList(timeline);
}
}
@@ -0,0 +1,145 @@
package de.jalin.fedipage;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FediPost implements Serializable {
private static final long serialVersionUID = 5177108486862427792L;
private boolean pinned;
private boolean boosted;
private String createdAt;
private String authorHandle;
private String authorDisplayName;
private String authorAvatarUrl;
private String authorUrl;
private String postUrl;
private String content;
private int boosts;
private int likes;
private int comments;
private final List<FediMedium> media;
public FediPost() {
setPinned(false);
setBoosted(false);
setCreatedAt("unknown");
setAuthorHandle("nobody");
setAuthorDisplayName("Nobody");
setAuthorAvatarUrl("undef");
setAuthorUrl("undef");
setPostUrl("undef");
setContent("<p>empty</p>");
setBoosts(0);
setLikes(0);
setComments(0);
this.media = new ArrayList<FediMedium>();
}
public boolean isPinned() {
return pinned;
}
public void setPinned(boolean pinned) {
this.pinned = pinned;
}
public boolean isBoosted() {
return boosted;
}
public void setBoosted(boolean boosted) {
this.boosted = boosted;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getAuthorHandle() {
return authorHandle;
}
public void setAuthorHandle(String authorHandle) {
this.authorHandle = authorHandle;
}
public String getAuthorDisplayName() {
return authorDisplayName;
}
public void setAuthorDisplayName(String authorDisplayName) {
this.authorDisplayName = authorDisplayName;
}
public String getAuthorAvatarUrl() {
return authorAvatarUrl;
}
public void setAuthorAvatarUrl(String authorAvatarUrl) {
this.authorAvatarUrl = authorAvatarUrl;
}
public String getPostUrl() {
return postUrl;
}
public void setPostUrl(String postUrl) {
this.postUrl = postUrl;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getBoosts() {
return boosts;
}
public void setBoosts(int boosts) {
this.boosts = boosts;
}
public int getLikes() {
return likes;
}
public void setLikes(int likes) {
this.likes = likes;
}
public int getComments() {
return comments;
}
public void setComments(int comments) {
this.comments = comments;
}
public void addFediMedium(final FediMedium medium) {
this.media.add(medium);
}
public List<FediMedium> getMedia() {
return Collections.unmodifiableList(this.media);
}
public void setAuthorUrl(final String authorUrl) {
this.authorUrl = authorUrl;
}
public String getAuthorUrl() {
return this.authorUrl;
}
}
@@ -43,10 +43,8 @@ public class FediServlet extends HttpServlet {
try { try {
final JSONLoader loader = new JSONLoader(instance, account); final JSONLoader loader = new JSONLoader(instance, account);
final VelocityContext context = new VelocityContext(); final VelocityContext context = new VelocityContext();
context.put("profile", loader.loadProfile());
context.put("pinned", loader.loadPinned());
context.put("timeline", loader.loadTimeline());
context.put("dateformatter", new PostDateFormatter()); context.put("dateformatter", new PostDateFormatter());
context.put("page", loader.loadPage());
final Template template = velocityEngine.getTemplate("/templates/profil.vm"); final Template template = velocityEngine.getTemplate("/templates/profil.vm");
response.setContentType("text/html;charset=UTF-8"); response.setContentType("text/html;charset=UTF-8");
+71 -41
View File
@@ -8,7 +8,6 @@ import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.Map;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -28,7 +27,74 @@ public class JSONLoader implements Serializable {
this.id = -1L; this.id = -1L;
} }
public JSONObject loadProfile() throws IOException, URISyntaxException { public FediPage loadPage() throws IOException, URISyntaxException {
final FediPage fediPage = new FediPage(account, instance);
loadFediProfile(fediPage);
loadStatuses(fediPage, 0, true);
loadStatuses(fediPage, 20, false);
return fediPage;
}
private void loadStatuses(final FediPage fedipage, final int limit, final boolean pinned) throws IOException, URISyntaxException {
String filter = "limit=" + limit;
if (pinned) {
filter = "pinned=true";
}
final JSONArray timelinePosts = loadJsonStatuses(filter);
for (int idx=0; idx<timelinePosts.length(); idx++) {
final JSONObject jsonPost = timelinePosts.getJSONObject(idx);
final FediPost post = new FediPost();
JSONObject jsonAccount = null;
JSONObject jsonStatus = null;
if (jsonPost.isNull("reblog")) {
post.setBoosted(false);
jsonStatus = jsonPost;
} else {
post.setBoosted(true);
jsonStatus = jsonPost.getJSONObject("reblog");
}
jsonAccount = jsonStatus.getJSONObject("account");
post.setAuthorAvatarUrl(jsonAccount.getString("avatar"));
post.setAuthorDisplayName(jsonAccount.getString("display_name"));
post.setAuthorHandle(jsonAccount.getString("acct"));
post.setAuthorUrl(jsonAccount.getString("url"));
post.setBoosts(jsonStatus.getInt("reblogs_count"));
post.setComments(jsonStatus.getInt("replies_count"));
post.setContent(jsonStatus.getString("content"));
post.setCreatedAt(jsonStatus.getString("created_at"));
post.setLikes(jsonStatus.getInt("favourites_count"));
post.setPinned(pinned);
post.setPostUrl(jsonStatus.getString("url"));
if (jsonStatus.getJSONArray("media_attachments").length() == 0) {
final JSONObject jsonCard = jsonStatus.getJSONObject("card");
if (jsonCard != null && jsonCard.getString("image") != null) {
final FediMedium medium = new FediMedium();
medium.setMediumType(FediMedium.CARD);
medium.setDescription(jsonCard.getString("description"));
medium.setMediumUrl(jsonCard.getString("url"));
medium.setMediumImage(jsonCard.getString("image"));
post.addFediMedium(medium);
}
} else {
}
fedipage.append(post);
}
}
private void loadFediProfile(final FediPage fediPage) throws IOException, URISyntaxException {
final JSONObject jsonProfile = loadJsonProfile();
fediPage.setDescription(jsonProfile.getString("note"));
fediPage.setDisplayName(jsonProfile.getString("display_name"));
fediPage.setFollowers(jsonProfile.getInt("followers_count"));
fediPage.setFollowing(jsonProfile.getInt("following_count"));
fediPage.setPosts(jsonProfile.getInt("statuses_count"));
fediPage.setUrl(jsonProfile.getString("url"));
fediPage.setAvatarUrl(jsonProfile.getString("avatar"));
fediPage.setHeaderUrl(jsonProfile.getString("header"));
}
private JSONObject loadJsonProfile() throws IOException, URISyntaxException {
final URI uri = new URI("https://" + instance + "/api/v1/accounts/lookup?acct=" + account); final URI uri = new URI("https://" + instance + "/api/v1/accounts/lookup?acct=" + account);
final URL url = uri.toURL(); final URL url = uri.toURL();
final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
@@ -44,31 +110,9 @@ public class JSONLoader implements Serializable {
return data; return data;
} }
public JSONArray loadPinned() throws IOException, URISyntaxException { private JSONArray loadJsonStatuses(final String filter) throws IOException, URISyntaxException {
final JSONArray pinnedStatusesArray = loadStatuses("pinned=true"); if (id < 0L) {
// final ArrayList<Status> pinnedStatuses = new ArrayList<>(); loadJsonProfile();
return pinnedStatusesArray;
}
public JSONArray loadTimeline() throws IOException, URISyntaxException {
final JSONArray lastStatusesArray = loadStatuses("limit=20");
// for (int idx=0; idx< lastStatusesArray.length(); idx++) {
// final Object object = lastStatusesArray.get(idx);
// JSONObject json = (JSONObject) object;
// final Status status = new Status();
// JSONObject account = json.getJSONObject("account");
// if (json.get("reblog") != null) {
// status.setBoostedBy(account.getString("display_name"));
// json = json.getJSONObject("reblog");
// }
// }
// final ArrayList<Status> latestStatuses = new ArrayList<>();
return lastStatusesArray;
}
private JSONArray loadStatuses(final String filter) throws IOException, URISyntaxException {
if (id < 0) {
loadProfile();
} }
final URI uri = new URI("https://" + instance + "/api/v1/accounts/" + id + "/statuses?" + filter); final URI uri = new URI("https://" + instance + "/api/v1/accounts/" + id + "/statuses?" + filter);
final URL url = uri.toURL(); final URL url = uri.toURL();
@@ -82,18 +126,4 @@ public class JSONLoader implements Serializable {
} }
return new JSONArray(content.toString()); return new JSONArray(content.toString());
} }
public static void main(String[] args) {
// final JSONLoader loader = new JSONLoader(args[0], args[1]);
final JSONLoader loader = new JSONLoader("krefeld.life", "darestiet");
try {
final JSONArray pinned = loader.loadTimeline();
for (Object obj : pinned) {
@SuppressWarnings("unchecked")
final Map<String, ?> json = (Map<String, ?>) obj;
System.out.println(json.toString());
}
} catch (IOException | URISyntaxException e) {
}
}
} }
+26 -147
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$profile.display_name - $profile.username</title> <title>$page.displayName - $page.username</title>
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -267,179 +267,58 @@
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<img id="banner" class="banner" alt="Banner" src="$profile.header"> <img id="banner" class="banner" alt="Banner" src="$page.headerUrl">
</div> </div>
<div class="profile-section"> <div class="profile-section">
<img id="avatar" class="profile-avatar" alt="Avatar" src="$profile.avatar"> <img id="avatar" class="profile-avatar" alt="Avatar" src="$page.avatarUrl">
<div class="profile-info"> <div class="profile-info">
<div id="displayName" class="display-name">$profile.display_name</div> <div id="displayName" class="display-name">$page.displayName</div>
<div id="username" class="username"><a href="$profile.url">@$profile.username</a></div> <div id="username" class="username"><a href="$page.url">$page.username</a></div>
<div id="bio" class="bio">$profile.note</div> <div id="bio" class="bio">$page.description</div>
<div class="stats"> <div class="stats">
<span id="followingCount">$profile.following_count Folgt</span> <span id="followingCount">$page.following Folgt</span>
<span id="followersCount">$profile.followers_count Follower</span> <span id="followersCount">$page.followers Follower</span>
<span id="statusesCount">$profile.statuses_count Beiträge</span> <span id="statusesCount">$page.posts Beiträge</span>
</div> </div>
</div> </div>
</div> </div>
<!--
<div class="controls">
<div class="checkbox-group">
<label>
<input type="checkbox" id="includeBoosts">
Geteilte Inhalte anzeigen
</label>
<label>
<input type="checkbox" id="includeReplies">
Antworten anzeigen
</label>
<button class="refresh-btn" onclick="loadPosts()">Aktualisieren</button>
</div>
</div>
-->
<!--
<div id="loading" class="loading">Lade Daten...</div>
<div id="error" class="error" style="display: none;"></div>
-->
<div id="posts" class="posts"> <div id="posts" class="posts">
#foreach( $post in $pinned ) #foreach( $post in $page.timeline )
#set ( $image_class = 'single' )
#set ( $post_date = $dateformatter.format( $post.created_at ) )
<div class="post"> <div class="post">
#set ( $image_class = 'single' )
#set ( $post_date = $dateformatter.format( $post.createdAt ) )
#if ( $post.pinned )
<div class="boost-indicator">📌 Angeheftet</div> <div class="boost-indicator">📌 Angeheftet</div>
#end
#if ( $post.boosted )
<div class="boost-indicator">🔄 $page.displayName hat geteilt</div>
#end
<div class="post-header"> <div class="post-header">
<img class="post-avatar" src="$post.account.avatar" alt="Avatar"> <img class="post-avatar" src="$post.authorAvatarUrl" alt="Avatar">
<div class="post-user"> <div class="post-user">
<div class="post-display-name">$post.account.display_name</div> <div class="post-display-name">$post.authorDisplayName</div>
<div class="post-username"><a href="$post.account.url">@$post.account.acct</a></div> <div class="post-username"><a href="$post.authorUrl">$post.authorHandle</a></div>
</div> </div>
<div class="post-date"><a href="$post.url" target="_blank">$post_date</a></div> <div class="post-date"><a href="$post.postUrl" target="_blank">$post_date</a></div>
</div> </div>
<div class="post-content">$post.content</div> <div class="post-content">$post.content</div>
#if( $post.media_attachments.length > 0 )
#if( $post.media_attachments.length > 1 )
#set ( $image_class = 'double' )
#end
#if( $post.media_attachments.length > 2 )
#set ( $image_class = 'multiple' )
#end
<div class="post-media"> <div class="post-media">
<div class="media-grid $image_class"> <div class="media-grid $image_class">
#foreach( $media_item in $post.media_attachments ) #foreach( $medium in $post.media )
<div class="media-item"> <div class="media-item">
<img src="$media_item.preview_url" alt="$media_item.description" onclick="window.open('$media_item.url', '_blank')"> <img src="$medium.mediumImage" alt="$medium.description" onclick="window.open('$medium.mediumUrl', '_blank')">
</div> </div>
#end #end
</div> </div>
</div> </div>
#else
#if( $post.card )
<div class="post-media">
<div class="media-grid single">
<div class="media-item">
<img src="$post.card.image" alt="$post.card.description" onclick="window.open('$post.card.url', '_blank')">
</div>
</div>
</div>
#end
#end
<div class="post-stats"> <div class="post-stats">
<span>🔄 $post.reblogs_count</span> <span>🔄 $post.boosts</span>
<span>⭐ $post.favourites_count</span> <span>⭐ $post.likes</span>
<span>💬 $post.replies_count</span> <span>💬 $post.comments</span>
</div> </div>
</div> </div>
#end
#foreach( $post in $timeline )
#set ( $image_class = 'single' )
<div class="post">
#if( $post.reblog.account )
#set ( $post_date = $dateformatter.format( $post.reblog.created_at ) )
#if( $post.reblog.media_attachments.length > 1 )
#set ( $image_class = 'double' )
#end
#if( $post.reblog.media_attachments.length > 2 )
#set ( $image_class = 'multiple' )
#end
<div class="boost-indicator">🔄 $post.account.display_name hat geteilt</div>
<div class="post-header">
<img class="post-avatar" src="$post.reblog.account.avatar" alt="Avatar">
<div class="post-user">
<div class="post-display-name">$post.reblog.account.display_name</div>
<div class="post-username"><a href="$post.reblog.account.url">@$post.reblog.account.acct</a></div>
</div>
<div class="post-date"><a href="$post.reblog.url" target="_blank">$post_date</a></div>
</div>
<div class="post-content">$post.reblog.content</div>
#if( $post.reblog.media_attachments.length > 0 )
<div class="post-media">
<div class="media-grid $image_class">
#foreach( $media_item in $post.reblog.media_attachments )
<div class="media-item">
<img src="$media_item.preview_url" alt="$media_item.description" onclick="window.open('$media_item.url', '_blank')">
</div>
#end
</div>
</div>
#else
#if( $post.reblog.card )
<div class="post-media">
<div class="media-grid single">
<div class="media-item">
<img src="$post.reblog.card.image" alt="$post.reblog.card.description" onclick="window.open('$post.reblog.card.url', '_blank')">
</div>
</div>
</div>
#end
#end
#else
#set ( $post_date = $dateformatter.format( $post.created_at ) )
#if( $post.media_attachments.length > 1 )
#set ( $image_class = 'double' )
#end
#if( $post.media_attachments.length > 2 )
#set ( $image_class = 'multiple' )
#end
<div class="post-header">
<img class="post-avatar" src="$post.account.avatar" alt="Avatar">
<div class="post-user">
<div class="post-display-name">$post.account.display_name</div>
<div class="post-username"><a href="$post.account.url">@$post.account.acct</a></div>
</div>
<div class="post-date"><a href="$post.url" target="_blank">$post_date</a></div>
</div>
<div class="post-content">$post.content</div>
#if( $post.media_attachments.length > 0 )
<div class="post-media">
<div class="media-grid $image_class">
#foreach( $media_item in $post.media_attachments )
<div class="media-item">
<img src="$media_item.preview_url" alt="$media_item.description" onclick="window.open('$media_item.url', '_blank')">
</div>
#end
</div>
</div>
#else
#if( $post.card )
<div class="post-media">
<div class="media-grid single">
<div class="media-item">
<img src="$post.card.image" alt="$post.card.description" onclick="window.open('$post.card.url', '_blank')">
</div>
</div>
</div>
#end
#end
<div class="post-stats">
<span>🔄 $post.reblogs_count</span>
<span>⭐ $post.favourites_count</span>
<span>💬 $post.replies_count</span>
</div>
#end
</div>
#end #end
</div> </div>
</div> </div>
+365
View File
@@ -0,0 +1,365 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$page.fediProfile.displayName - $page.fediProfile.username</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a1a;
color: #ffffff;
line-height: 1.6;
}
.container {
max-width: 960px;
margin: 0 auto;
background: #282c37;
min-height: 100vh;
}
.header {
position: relative;
height: 200px;
background: linear-gradient(45deg, #6364ff, #563acc);
overflow: hidden;
}
.banner {
width: 100%;
height: 100%;
object-fit: cover;
}
.profile-section {
position: relative;
padding: 0 20px 20px;
background: #282c37;
}
.profile-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
border: 4px solid #282c37;
margin-top: -40px;
background: #fff;
}
.profile-info {
margin-top: 15px;
}
.display-name {
font-size: 20px;
font-weight: bold;
margin-bottom: 5px;
}
.username {
color: #9baec8;
margin-bottom: 10px;
}
.username a {
color: #9baec8;
text-decoration: none;
}
.bio {
margin-bottom: 15px;
line-height: 1.5;
}
.stats {
display: flex;
gap: 20px;
font-size: 14px;
color: #9baec8;
}
.controls {
padding: 20px;
background: #313543;
border-bottom: 1px solid #393f4f;
}
.checkbox-group {
display: flex;
gap: 20px;
align-items: center;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: #9baec8;
}
.checkbox-group input[type="checkbox"] {
accent-color: #6364ff;
}
.posts {
background: #282c37;
}
.post {
border-bottom: 1px solid #393f4f;
padding: 15px 20px;
}
.post-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.post-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.post-user {
flex: 1;
}
.post-display-name {
font-weight: bold;
font-size: 14px;
}
.post-username {
color: #9baec8;
font-size: 13px;
}
.post-username a {
color: #9baec8;
font-size: 13px;
text-decoration: none;
}
.post-date {
color: #9baec8;
font-size: 13px;
}
.post-date a {
color: #9baec8;
font-size: 13px;
text-decoration: none;
}
.post-content {
margin-bottom: 10px;
}
.post-content p {
margin-bottom: 10px;
}
.post-content a {
color: #6364ff;
text-decoration: none;
}
.post-content a:hover {
text-decoration: underline;
}
.hashtag {
color: #00d4aa;
}
.mention {
color: #6364ff;
}
.post-media {
margin-top: 10px;
}
.media-grid {
display: grid;
gap: 4px;
border-radius: 8px;
overflow: hidden;
}
.media-grid.single {
grid-template-columns: 1fr;
}
.media-grid.double {
grid-template-columns: 1fr 1fr;
}
.media-grid.multiple {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.media-item {
position: relative;
background: #000;
}
.media-item img {
width: 100%;
height: 360px;
object-fit: cover;
cursor: pointer;
}
.post-stats {
display: flex;
gap: 15px;
margin-top: 10px;
color: #9baec8;
font-size: 13px;
}
.boost-indicator {
color: #00d4aa;
font-size: 13px;
margin-bottom: 10px;
}
.loading {
text-align: center;
padding: 40px;
color: #9baec8;
}
.error {
text-align: center;
padding: 40px;
color: #ff5050;
}
.refresh-btn {
background: #6364ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
}
.refresh-btn:hover {
background: #5a5bef;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<img id="banner" class="banner" alt="Banner" src="$page.fediProfile.headerUrl">
</div>
<div class="profile-section">
<img id="avatar" class="profile-avatar" alt="Avatar" src="$page.fediProfile.avatarUrl">
<div class="profile-info">
<div id="displayName" class="display-name">$page.fediProfile.displayName</div>
<div id="username" class="username"><a href="$profile.url">$page.fediProfile.username</a></div>
<div id="bio" class="bio">$page.fediProfile.description</div>
<div class="stats">
<span id="followingCount">$page.fediProfile.following Folgt</span>
<span id="followersCount">$page.fediProfile.followers Follower</span>
<span id="statusesCount">$page.fediProfile.posts Beiträge</span>
</div>
</div>
</div>
<!--
<div class="controls">
<div class="checkbox-group">
<label>
<input type="checkbox" id="includeBoosts">
Geteilte Inhalte anzeigen
</label>
<label>
<input type="checkbox" id="includeReplies">
Antworten anzeigen
</label>
<button class="refresh-btn" onclick="loadPosts()">Aktualisieren</button>
</div>
</div>
-->
<!--
<div id="loading" class="loading">Lade Daten...</div>
<div id="error" class="error" style="display: none;"></div>
-->
<div id="posts" class="posts">
#foreach( $post in $page.fediTimeline )
#set ( $image_class = 'single' )
#set ( $post_date = $dateformatter.format( $post.createdAt ) )
<div class="post">
#if ( $post.pinned )
<div class="boost-indicator">📌 Angeheftet</div>
#end
#if ( $post.boosted )
<div class="boost-indicator">🔄 $page.fediProfile.displayName hat geteilt</div>
#end
<div class="post-header">
<img class="post-avatar" src="$post.authorAvatarUrl" alt="Avatar">
<div class="post-user">
<div class="post-display-name">$post.authorDisplayName</div>
<div class="post-username"><a href="#">$post.authorHandle</a></div>
</div>
<div class="post-date"><a href="$post.postUrl" target="_blank">$post_date</a></div>
</div>
<div class="post-content">$post.content</div>
#if( $post.media_attachments.length > 0 )
#if( $post.media_attachments.length > 1 )
#set ( $image_class = 'double' )
#end
#if( $post.media_attachments.length > 2 )
#set ( $image_class = 'multiple' )
#end
<div class="post-media">
<div class="media-grid $image_class">
#foreach( $media_item in $post.media_attachments )
<div class="media-item">
<img src="$media_item.preview_url" alt="$media_item.description" onclick="window.open('$media_item.url', '_blank')">
</div>
#end
</div>
</div>
#else
#if( $post.card )
<div class="post-media">
<div class="media-grid single">
<div class="media-item">
<img src="$post.card.image" alt="$post.card.description" onclick="window.open('$post.card.url', '_blank')">
</div>
</div>
</div>
#end
#end
<div class="post-stats">
<span>🔄 $post.boosts</span>
<span>⭐ $post.likes</span>
<span>💬 $post.comments</span>
js </div>
</div>
#end
</div>
</div>
</body>
</html>