code refactor
Some checks failed
CI / Check / Test (push) Has been cancelled

Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
2026-10-26 09:36:23 +00:00
parent 2ce7db12c2
commit 017b58d6fa
804 changed files with 33423 additions and 28955 deletions

View File

@@ -0,0 +1,10 @@
<div class="feed-controls">
<select name="sort_by" onchange="this.form.submit()">
<option value="date"{% if sort_by == "date" %} selected{% endif %}>Date: newest first</option>
<option value="date_asc"{% if sort_by == "date_asc" %} selected{% endif %}>Date: oldest first</option>
<option value="rating"{% if sort_by == "rating" %} selected{% endif %}>Rating: highest first</option>
<option value="rating_asc"{% if sort_by == "rating_asc" %} selected{% endif %}>Rating: lowest first</option>
</select>
<input type="text" name="search" value="{{ search }}" placeholder="Search movies...">
<button type="submit" class="btn-search">Search</button>
</div>

View File

@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block content %}
<form method="get" class="feed-filters" action="/">
{% if ctx.user_email.is_some() %}
<label class="pill{% if filter == "all" %} active{% endif %}">
<input type="radio" name="filter" value="all"
{% if filter == "all" %}checked{% endif %}
onchange="this.form.submit()">
Global
</label>
<label class="pill{% if filter == "following" %} active{% endif %}">
<input type="radio" name="filter" value="following"
{% if filter == "following" %}checked{% endif %}
onchange="this.form.submit()">
Following
</label>
{% endif %}
{% include "_filter_controls.html" %}
{% if filter != "all" || sort_by != "date" || !search.is_empty() %}
<a href="/" class="clear-filters">Clear</a>
{% endif %}
<input type="hidden" name="limit" value="{{ limit }}">
</form>
<div class="diary">
{% for entry in entries %}
<article class="entry">
{% if let Some(poster) = entry.movie().poster_path() %}
<div class="poster">
<img src="{{ poster.value()|poster_src }}" alt="">
</div>
{% endif %}
<div class="entry-body">
<div class="entry-title">
<a href="/movies/{{ entry.movie().id().value() }}" class="movie-title-link">{{ entry.movie().title().value() }}</a>
<span class="year">({{ entry.movie().release_year().value() }})</span>
</div>
{% if let Some(dir) = entry.movie().director() %}
<div class="director">{{ dir }}</div>
{% endif %}
<div class="rating">
{% for filled in entry.review().stars() %}
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %}
</div>
{% if let Some(wm) = entry.review().watch_medium() %}
<span class="watch-medium">{{ wm.label() }}</span>
{% endif %}
{% if let Some(comment) = entry.review().comment() %}
{% if entry.review().spoilers() %}
<details class="comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }}
</details>
{% else %}
<div class="comment">{{ comment.value() }}</div>
{% endif %}
{% endif %}
<div class="feed-meta">
{% match entry.review().source() %}
{% when ReviewSource::Remote with { actor_url } %}
<a href="{{ actor_url }}" class="feed-user" target="_blank" rel="noopener noreferrer">{{ entry.user_display_name() }}</a>
<span class="feed-time">{{ entry.review().watched_at().format("%b %-d, %Y") }}</span>
<span class="remote-badge">&#8599; federated</span>
{% when ReviewSource::Local %}
<a href="/users/{{ entry.review().user_id().value() }}" class="feed-user">{{ entry.user_display_name() }}</a>
<span class="feed-time">{{ entry.review().watched_at().format("%b %-d, %Y") }}</span>
{% endmatch %}
</div>
{% if ctx.is_current_user(entry.review().user_id().value()) %}
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
<input type="hidden" name="redirect_after" value="/?offset={{ current_offset }}{{ self.filter_qs() }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Delete</button>
</form>
{% endif %}
</div>
</article>
{% else %}
<p class="empty">No movies logged yet.</p>
{% endfor %}
</div>
<nav class="pagination">
{% if current_offset >= limit %}
<a href="/?offset={{ current_offset - limit }}{{ self.filter_qs() }}" class="page-nav">&larr; Prev</a>
{% endif %}
{% for item in page_items %}
{% if item.is_ellipsis %}
<span class="page-ellipsis">&hellip;</span>
{% elif item.is_current %}
<span class="page-num current">{{ item.number + 1 }}</span>
{% else %}
<a href="/?offset={{ item.number * limit }}{{ self.filter_qs() }}" class="page-num">{{ item.number + 1 }}</a>
{% endif %}
{% endfor %}
{% if has_more %}
<a href="/?offset={{ current_offset + limit }}{{ self.filter_qs() }}" class="page-nav">Next &rarr;</a>
{% endif %}
</nav>
{% endblock %}

View File

@@ -0,0 +1,70 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ ctx.page_title }}</title>
<meta
name="description"
content="A personal movie diary — track what you watch, rate and review films."
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Movies Diary" />
<meta property="og:title" content="{{ ctx.page_title }}" />
<meta property="og:url" content="{{ ctx.canonical_url }}" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ ctx.page_title }}" />
<link rel="canonical" href="{{ ctx.canonical_url }}" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="icon" type="image/webp" href="/static/logo.webp" />
<link rel="apple-touch-icon" href="/static/logo.webp" />
<link rel="manifest" href="/static/manifest.json" />
<meta name="theme-color" content="#e5c034" />
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<header>
<a href="/" class="site-title">Movies Diary</a>
<nav>
<a href="/">Feed</a>
<a href="/users">Users</a>
{% if let Some(uid) = ctx.user_id %}
<a href="/users/{{ uid }}">Profile{% if ctx.pending_follow_count > 0 %} ({{ ctx.pending_follow_count }}){% endif %}</a>
<a href="/reviews/new">Add Review</a>
<a href="/import">Import</a>
<a href="/watch-queue">Queue</a>
<a href="/logout">Logout</a>
{% else %}
<a href="/login">Login</a>
{% if ctx.register_enabled %}
<a href="/register">Register</a>
{% endif %} {% endif %}
</nav>
</header>
<main>{% block content %}{% endblock %}</main>
<footer class="site-footer">
<span class="footer-made">Made with passion</span>
<span class="footer-sep">·</span>
<a href="/feed.rss" class="footer-link">RSS</a>
{% if let Some(rss) = ctx.page_rss_url %}
<span class="footer-sep">·</span>
<a href="{{ rss }}" class="footer-link">Profile RSS</a>
{% endif %}
{% if let Some(uid) = ctx.user_id %}
<span class="footer-sep">·</span>
<a href="/users/{{ uid }}" class="footer-link">My Profile</a>
<span class="footer-sep">·</span>
<a href="/users/{{ uid }}/feed.rss" class="footer-link">My RSS</a>
{% endif %}
<span class="footer-sep">·</span>
<a href="/docs" target="_blank" class="footer-link">API Docs</a>
<span class="footer-sep">·</span>
<a href="/app/" class="footer-link">Mobile App</a>
</footer>
</body>
</html>

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block content %}
<h2>Blocked Users</h2>
{% if actors.is_empty() %}
<p>No users blocked.</p>
{% else %}
<ul class="following-list">
{% for a in actors %}
<li class="following-item">
{% if let Some(avatar) = a.avatar_url %}
<img src="{{ avatar }}" alt="avatar" style="width:32px;height:32px;border-radius:50%" />
{% endif %}
<strong>{{ a.handle }}</strong>{% if let Some(name) = a.display_name %} ({{ name }}){% endif %}
<form method="POST" action="/social/unblock" style="display:inline">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}" />
<input type="hidden" name="actor_url" value="{{ a.url }}" />
<button type="submit">Unblock</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block content %}
<h2>Blocked Domains</h2>
<form method="POST" action="/admin/blocked-domains">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}" />
<div>
<label for="domain">Domain</label>
<input id="domain" type="text" name="domain" placeholder="spam.example.com" required />
</div>
<div>
<label for="reason">Reason (optional)</label>
<input id="reason" type="text" name="reason" />
</div>
<button type="submit">Block Domain</button>
</form>
{% if domains.is_empty() %}
<p>No domains blocked.</p>
{% else %}
<ul>
{% for d in domains %}
<li>
<strong>{{ d.domain }}</strong>{% if let Some(r) = d.reason %} — {{ r }}{% endif %}
({{ d.blocked_at }})
<form method="POST" action="/admin/blocked-domains/remove" style="display:inline">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}" />
<input type="hidden" name="domain" value="{{ d.domain }}" />
<button type="submit">Unblock</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block content %}
<div class="diary">
{% for entry in entries %}
<article class="entry">
{% if let Some(poster) = entry.movie().poster_path() %}
<div class="poster">
<img src="{{ poster.value()|poster_src }}" alt="">
</div>
{% endif %}
<div class="entry-body">
<div class="entry-title">
{{ entry.movie().title().value() }}
<span class="year">({{ entry.movie().release_year().value() }})</span>
</div>
{% if let Some(dir) = entry.movie().director() %}
<div class="director">{{ dir }}</div>
{% endif %}
<div class="rating">
<span class="star {% if entry.review().rating().value() >= 1 %}filled{% else %}empty{% endif %}">★</span>
<span class="star {% if entry.review().rating().value() >= 2 %}filled{% else %}empty{% endif %}">★</span>
<span class="star {% if entry.review().rating().value() >= 3 %}filled{% else %}empty{% endif %}">★</span>
<span class="star {% if entry.review().rating().value() >= 4 %}filled{% else %}empty{% endif %}">★</span>
<span class="star {% if entry.review().rating().value() >= 5 %}filled{% else %}empty{% endif %}">★</span>
</div>
{% if let Some(comment) = entry.review().comment() %}
<div class="comment">{{ comment.value() }}</div>
{% endif %}
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
{% if let Some(uid) = ctx.user_id %}
{% if *uid == entry.review().user_id().value() %}
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Delete</button>
</form>
{% endif %}
{% endif %}
</div>
</article>
{% else %}
<p class="empty">No movies logged yet.</p>
{% endfor %}
</div>
<nav class="pagination">
{% if current_offset > 0 %}
<a href="/?offset={{ current_offset - limit }}" class="page-nav">&larr; Prev</a>
{% endif %}
{% for item in page_items %}
{% if item.is_ellipsis %}
<span class="page-ellipsis">&hellip;</span>
{% elif item.is_current %}
<span class="page-num current">{{ item.number + 1 }}</span>
{% else %}
<a href="/?offset={{ item.number * limit }}" class="page-num">{{ item.number + 1 }}</a>
{% endif %}
{% endfor %}
{% if has_more %}
<a href="/?offset={{ current_offset + limit }}" class="page-nav">Next &rarr;</a>
{% endif %}
</nav>
{% endblock %}

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/embed.css" />
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,196 @@
{% extends "embed_base.html" %}
{% block content %}
<div class="embed-profile">
<div class="embed-stats-header">
<div class="embed-profile-name">{{ profile_display_name }}</div>
<div class="embed-stats-grid">
<div class="embed-stat-tile">
<div class="embed-stat-value">{{ stats.total_movies }}</div>
<div class="embed-stat-label">movies</div>
</div>
<div class="embed-stat-tile">
<div class="embed-stat-value">{{ avg_rating_display }}★</div>
<div class="embed-stat-label">avg rating</div>
</div>
<div class="embed-stat-tile">
<div class="embed-stat-value">{{ favorite_director_display }}</div>
<div class="embed-stat-label">fav director</div>
</div>
<div class="embed-stat-tile">
<div class="embed-stat-value">{{ most_active_month_display }}</div>
<div class="embed-stat-label">most active</div>
</div>
</div>
</div>
<div class="embed-view-tabs">
<a href="?view=recent&embed=true" class="embed-view-tab {% if view == "recent" %}active{% endif %}">Recent</a>
<a href="?view=ratings&embed=true" class="embed-view-tab {% if view == "ratings" %}active{% endif %}">Top Rated</a>
<a href="?view=history&embed=true" class="embed-view-tab {% if view == "history" %}active{% endif %}">History</a>
<a href="?view=trends&embed=true" class="embed-view-tab {% if view == "trends" %}active{% endif %}">Trends</a>
</div>
{% if view == "recent" || view == "ratings" %}
<form method="get" class="embed-sort-form" action="/users/{{ profile_user_id }}">
<input type="hidden" name="view" value="{{ view }}">
<input type="hidden" name="limit" value="{{ limit }}">
<input type="hidden" name="embed" value="1">
<div class="embed-sort-controls">
<select name="sort_by" onchange="this.form.submit()">
<option value="date"{% if sort_by == "date" %} selected{% endif %}>Date: newest first</option>
<option value="date_asc"{% if sort_by == "date_asc" %} selected{% endif %}>Date: oldest first</option>
<option value="rating"{% if sort_by == "rating" %} selected{% endif %}>Rating: highest first</option>
<option value="rating_asc"{% if sort_by == "rating_asc" %} selected{% endif %}>Rating: lowest first</option>
</select>
</div>
</form>
{% endif %}
{% if view == "history" %}
{% if let Some(hist) = history %}
<div class="embed-heatmap-section">
<div class="embed-heatmap-label">Movies watched this year</div>
<div class="embed-heatmap">
{% for cell in heatmap %}
<div class="embed-heatmap-cell" style="--alpha: {{ cell.alpha }}">
<div class="embed-heatmap-count">{{ cell.count }}</div>
<div class="embed-heatmap-month">{{ cell.month_label }}</div>
</div>
{% endfor %}
</div>
</div>
{% for month in hist %}
<div class="embed-history-month">
<h3 class="embed-month-heading">{{ month.month_label }} <span class="embed-month-count">{{ month.count }}</span></h3>
<div class="embed-diary">
{% for entry in month.entries %}
<article class="embed-entry">
{% if let Some(poster) = entry.movie().poster_path() %}
<div class="embed-poster"><img src="{{ poster.value()|poster_src }}" alt="" loading="lazy"></div>
{% endif %}
<div class="embed-entry-body">
<div class="embed-entry-title"><a href="/movies/{{ entry.movie().id().value() }}" class="embed-movie-link" target="_blank" rel="noopener">{{ entry.movie().title().value() }}</a> <span class="embed-year">({{ entry.movie().release_year().value() }})</span></div>
{% if let Some(dir) = entry.movie().director() %}<div class="embed-director">{{ dir }}</div>{% endif %}
<div class="embed-rating">
{% for filled in entry.review().stars() %}
<span class="embed-star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %}
</div>
<div class="embed-watched-at">{{ entry.review().watched_at().format("%b %-d") }}</div>
</div>
</article>
{% endfor %}
</div>
</div>
{% else %}
<p class="embed-empty">No movies logged yet.</p>
{% endfor %}
{% endif %}
{% elif view == "trends" %}
{% if let Some(t) = trends %}
<div class="embed-trends-section">
{% if !monthly_rating_rows.is_empty() %}
<div class="embed-chart-block">
<div class="embed-chart-label">Average rating per month</div>
<div class="embed-bar-chart">
{% for row in monthly_rating_rows %}
<div class="embed-bar-col">
<div class="embed-bar-value">{{ "{:.1}"|format(row.rating.avg_rating) }}</div>
<div class="embed-bar-fill" style="height: {{ row.bar_height_px }}px"></div>
<div class="embed-bar-month">{{ row.rating.month_label }}</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if !t.top_directors.is_empty() %}
<div class="embed-chart-block">
<div class="embed-chart-label">Most watched directors</div>
<div class="embed-director-chart">
{% for d in t.top_directors %}
<div class="embed-director-row">
<div class="embed-director-name">{{ d.director }}</div>
<div class="embed-director-bar">
{% if t.max_director_count > 0 %}
<div class="embed-director-bar-fill" style="width: {{ d.count * 100 / t.max_director_count }}%"></div>
{% else %}
<div class="embed-director-bar-fill" style="width: 0%"></div>
{% endif %}
</div>
<div class="embed-director-count">{{ d.count }}</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}
{% else %}
{% if let Some(paged) = entries %}
<div class="embed-diary">
{% for entry in paged.items %}
<article class="embed-entry">
{% if let Some(poster) = entry.movie().poster_path() %}
<div class="embed-poster">
<img src="{{ poster.value()|poster_src }}" alt="" loading="lazy">
</div>
{% endif %}
<div class="embed-entry-body">
<div class="embed-entry-title">
<a href="/movies/{{ entry.movie().id().value() }}" class="embed-movie-link" target="_blank" rel="noopener">{{ entry.movie().title().value() }}</a>
<span class="embed-year">({{ entry.movie().release_year().value() }})</span>
</div>
{% if let Some(dir) = entry.movie().director() %}
<div class="embed-director">{{ dir }}</div>
{% endif %}
<div class="embed-rating">
{% for filled in entry.review().stars() %}
<span class="embed-star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %}
</div>
{% if let Some(comment) = entry.review().comment() %}
{% if entry.review().spoilers() %}
<details class="embed-comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }}
</details>
{% else %}
<div class="embed-comment">{{ comment.value() }}</div>
{% endif %}
{% endif %}
<div class="embed-watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
</div>
</article>
{% else %}
<p class="embed-empty">No reviews yet.</p>
{% endfor %}
</div>
<nav class="embed-pagination">
{% if current_offset >= limit %}
<a href="/users/{{ profile_user_id }}?offset={{ current_offset - limit }}{{ self.filter_qs() }}" class="embed-page-nav">&larr; Prev</a>
{% endif %}
{% for item in page_items %}
{% if item.is_ellipsis %}
<span class="embed-page-ellipsis">&hellip;</span>
{% elif item.is_current %}
<span class="embed-page-num current">{{ item.number + 1 }}</span>
{% else %}
<a href="/users/{{ profile_user_id }}?offset={{ item.number * limit }}{{ self.filter_qs() }}" class="embed-page-num">{{ item.number + 1 }}</a>
{% endif %}
{% endfor %}
{% if has_more %}
<a href="/users/{{ profile_user_id }}?offset={{ current_offset + limit }}{{ self.filter_qs() }}" class="embed-page-nav">Next &rarr;</a>
{% endif %}
</nav>
{% endif %}
{% endif %}
<div class="embed-footer">
<a href="{{ profile_url }}" target="_blank" rel="noopener">powered by Movies Diary</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block content %}
<h2>Followers</h2>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
{% if actors.is_empty() %}
<p>No followers yet.</p>
{% else %}
<ul class="following-list">
{% for actor in actors %}
<li class="following-item">
{% if let Some(avatar) = actor.avatar_url %}
<img src="{{ avatar }}" alt="" style="width:32px;height:32px;border-radius:50%;vertical-align:middle;margin-right:6px" />
{% endif %}
<strong>{{ actor.handle }}</strong>
{% if let Some(name) = actor.display_name %}
({{ name }})
{% endif %}
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">View profile ↗</a>
<form method="POST" action="/users/{{ user_id }}/followers/remove" style="display:inline">
<input type="hidden" name="actor_url" value="{{ actor.url }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Remove</button>
</form>
<form method="POST" action="/social/block" style="display:inline">
<input type="hidden" name="actor_url" value="{{ actor.url }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Block</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block content %}
<h2>Following</h2>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
<form method="POST" action="/users/{{ user_id }}/follow" class="follow-form">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<input type="hidden" name="redirect_after" value="/users/{{ user_id }}/following-list">
<input type="text" name="handle" placeholder="@user@instance.tld" required>
<button type="submit">Follow</button>
</form>
{% if !pending_actors.is_empty() %}
<h3>Requested ({{ pending_actors.len() }})</h3>
<ul class="following-list">
{% for actor in pending_actors %}
<li class="following-item">
{% if let Some(avatar) = actor.avatar_url %}
<img src="{{ avatar }}" alt="" style="width:32px;height:32px;border-radius:50%;vertical-align:middle;margin-right:6px" />
{% endif %}
<strong>{{ actor.handle }}</strong>
{% if let Some(name) = actor.display_name %}
({{ name }})
{% endif %}
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">View profile ↗</a>
<form method="POST" action="/users/{{ user_id }}/unfollow" style="display:inline">
<input type="hidden" name="actor_url" value="{{ actor.url }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Cancel request</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}
{% if actors.is_empty() %}
<p>Not following anyone yet. Follow remote users from your <a href="/users/{{ user_id }}">profile page</a>.</p>
{% else %}
<ul class="following-list">
{% for actor in actors %}
<li class="following-item">
{% if let Some(avatar) = actor.avatar_url %}
<img src="{{ avatar }}" alt="" style="width:32px;height:32px;border-radius:50%;vertical-align:middle;margin-right:6px" />
{% endif %}
<strong>{{ actor.handle }}</strong>
{% if let Some(name) = actor.display_name %}
({{ name }})
{% endif %}
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">View profile ↗</a>
<form method="POST" action="/users/{{ user_id }}/unfollow" style="display:inline">
<input type="hidden" name="actor_url" value="{{ actor.url }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Unfollow</button>
</form>
<form method="POST" action="/social/block" style="display:inline">
<input type="hidden" name="actor_url" value="{{ actor.url }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Block</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block content %}
<h1>Map Columns</h1>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
<p>Showing up to 5 sample rows. Map each column to a diary field.</p>
<form method="POST" action="/import/{{ session_id }}/mapping">
<table>
<thead>
<tr>
{% for col in columns %}<th>{{ col }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for row in sample_rows %}
<tr>{% for cell in row %}<td>{{ cell }}</td>{% endfor %}</tr>
{% endfor %}
</tbody>
</table>
{% for col in columns %}
<fieldset>
<legend>{{ col }}</legend>
<label>Maps to
<select name="mapping_{{ loop.index0 }}_field">
<option value="">— skip —</option>
{% for (val, label) in domain_fields %}
<option value="{{ val }}">{{ label }}</option>
{% endfor %}
</select>
</label>
<label>Rating scale
<select name="mapping_{{ loop.index0 }}_scale">
<option value="1.0">Same (0–5)</option>
<option value="0.5">10-point (/2)</option>
<option value="0.05">Percentage (/20)</option>
</select>
</label>
<label>Date format
<input type="text" name="mapping_{{ loop.index0 }}_datefmt" placeholder="%Y-%m-%d">
</label>
<input type="hidden" name="mapping_{{ loop.index0 }}_col" value="{{ col }}">
</fieldset>
{% endfor %}
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Preview Import</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block content %}
<h1>Preview Import</h1>
<form method="POST" action="/import/{{ session_id }}/confirm">
<table>
<thead>
<tr>
<th>Include?</th>
{% for col in columns %}<th>{{ col }}</th>{% endfor %}
<th>Status</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>
{% match row.status %}
{% when ImportRowStatus::Invalid with (_e) %}
<input type="checkbox" disabled>
{% when _ %}
<input type="checkbox" name="confirmed" value="{{ row.index }}" checked>
{% endmatch %}
</td>
{% for cell in row.cells %}<td>{{ cell }}</td>{% endfor %}
<td>
{% match row.status %}
{% when ImportRowStatus::Valid %}&#10003;
{% when ImportRowStatus::Duplicate %}&#9888; duplicate
{% when ImportRowStatus::Invalid with (e) %}&#10007; {{ e }}
{% endmatch %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<label>Save this mapping as a profile?
<input type="text" name="profile_name" placeholder="e.g. Letterboxd">
</label>
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Import Selected</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block content %}
<h1>Import Reviews</h1>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
{% if !profiles.is_empty() %}
<section>
<h2>Saved Profiles</h2>
<ul>
{% for p in profiles %}
<li>
{{ p.name }}
<form method="POST" action="/import/profiles/{{ p.id }}/delete" style="display:inline">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Delete</button>
</form>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
<h2>Upload File</h2>
<form method="POST" action="/import/upload" enctype="multipart/form-data">
<label>
File (CSV, TSV, JSON, XLSX)<br>
<input type="file" name="file" accept=".csv,.tsv,.json,.xlsx" required>
</label>
<label>
Format<br>
<select name="format">
<option value="csv">CSV / TSV</option>
<option value="json">JSON</option>
<option value="xlsx">XLSX</option>
</select>
</label>
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Upload</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,92 @@
{% extends "base.html" %}
{% block content %}
<h1>Integrations</h1>
<p style="font-size:.85em;opacity:.7;margin-bottom:1rem">
<a href="/settings/profile">Profile Settings</a>
</p>
<section style="margin-bottom:2rem">
<h2 style="font-size:1.1em">Jellyfin / Plex Webhook</h2>
<p style="opacity:.7;font-size:.9em">
Automatically log movies you finish watching. Configure your media server's
webhook plugin to POST to the URL below.
</p>
<div style="margin:1rem 0;padding:0.75rem 1rem;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px">
<label style="display:block;font-size:0.8em;opacity:.6;margin-bottom:0.25rem">Jellyfin Webhook URL</label>
<code style="word-break:break-all">{{ webhook_base_url }}/api/v1/webhooks/jellyfin</code>
</div>
<div style="margin:1rem 0;padding:0.75rem 1rem;background:var(--glass-bg);border:1px solid var(--glass-border);border-radius:8px">
<label style="display:block;font-size:0.8em;opacity:.6;margin-bottom:0.25rem">Plex Webhook URL (append token as query param)</label>
<code style="word-break:break-all">{{ webhook_base_url }}/api/v1/webhooks/plex?token=YOUR_TOKEN</code>
</div>
<details style="margin:1rem 0;font-size:.85em;opacity:.8">
<summary style="cursor:pointer">Jellyfin setup</summary>
<ol style="margin-top:0.5rem;padding-left:1.2rem">
<li>Install the <strong>Webhook</strong> plugin (Dashboard &rarr; Plugins &rarr; Catalog)</li>
<li>Add Generic Destination with the Jellyfin URL above</li>
<li>Add header: <code>Authorization</code> = <code>Bearer YOUR_TOKEN</code></li>
<li>Check <strong>Send All Properties</strong></li>
<li>Notification Type: <strong>Playback Stop</strong> only</li>
<li>Item Type: <strong>Movies</strong> only</li>
</ol>
</details>
<details style="margin:1rem 0;font-size:.85em;opacity:.8">
<summary style="cursor:pointer">Plex setup (requires Plex Pass)</summary>
<ol style="margin-top:0.5rem;padding-left:1.2rem">
<li>Go to Settings &rarr; Webhooks in your Plex server</li>
<li>Add the Plex URL above, replacing <code>YOUR_TOKEN</code> with your generated token</li>
<li>Plex automatically sends scrobble events when a movie is watched to 90%+</li>
</ol>
</details>
{% if let Some(token) = new_token %}
<div style="margin:1rem 0;padding:0.75rem 1rem;background:rgba(229,192,52,.1);border:1px solid rgba(229,192,52,.3);border-radius:8px">
<strong>New token (copy now — shown only once):</strong><br>
<code style="word-break:break-all;font-size:1.1em">{{ token }}</code>
</div>
{% endif %}
<form method="post" action="/settings/integrations/generate" style="display:flex;gap:0.5rem;align-items:flex-end;margin:1rem 0">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<input type="hidden" name="provider" value="jellyfin">
<div style="flex:1;min-width:150px">
<label style="display:block;font-size:0.8em;opacity:.6;margin-bottom:0.25rem">Label (optional)</label>
<input type="text" name="label" placeholder="e.g. Living Room Server" style="width:100%;box-sizing:border-box">
</div>
<button type="submit" class="btn-small" style="height:2.25rem">Generate Token</button>
</form>
</section>
{% if !tokens.is_empty() %}
<section>
<h2 style="font-size:1.1em">Active Tokens</h2>
<div class="diary">
{% for t in tokens %}
<article class="entry" style="padding:0.75rem 1rem">
<div class="entry-body">
<div class="entry-title" style="font-size:0.95em">
{{ t.provider }}{% if let Some(l) = &t.label %} — {{ l }}{% endif %}
</div>
<div class="feed-meta" style="margin-top:0.3rem">
<span class="feed-time">Created {{ t.created_at }}</span>
{% if let Some(used) = &t.last_used_at %}
<span class="feed-time" style="margin-left:0.5rem">Last used {{ used }}</span>
{% else %}
<span class="feed-time" style="margin-left:0.5rem;opacity:.5">Never used</span>
{% endif %}
</div>
<form method="post" action="/settings/integrations/{{ t.id }}/revoke" style="margin-top:0.5rem">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit" class="btn-small" style="color:#e57a7a;border-color:rgba(229,122,122,.3)">Revoke</button>
</form>
</div>
</article>
{% endfor %}
</div>
</section>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block content %}
<h1>Login</h1>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
<form method="POST" action="/login">
<label>
Email<br>
<input type="email" name="email" required autocomplete="email">
</label>
<label>
Password<br>
<input type="password" name="password" required autocomplete="current-password">
</label>
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Login</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,181 @@
{% extends "base.html" %}
{% block content %}
<div class="movie-detail">
{# ── Hero ── #}
<article class="entry" style="margin-bottom:1.5rem">
{% if let Some(poster) = movie.poster_path() %}
<div class="poster"><img src="{{ poster.value()|poster_src }}" alt=""></div>
{% endif %}
<div class="entry-body">
<div class="entry-title">
{{ movie.title().value() }}
<span class="year">({{ movie.release_year().value() }})</span>
</div>
<div class="movie-meta">
{% if let Some(dir) = movie.director() %}{{ dir }}{% endif %}
{% if let Some(p) = profile %}
{% if let Some(runtime) = p.runtime_minutes %}· {{ runtime }} min{% endif %}
{% if let Some(lang) = &p.original_language %}· {{ lang|upper }}{% endif %}
{% endif %}
</div>
{% if let Some(p) = profile %}
{% if !p.genres.is_empty() %}
<div class="genre-pills">
{% for g in &p.genres %}<span class="genre-pill">{{ g.name }}</span>{% endfor %}
</div>
{% endif %}
{% if let Some(tagline) = &p.tagline %}{% if !tagline.is_empty() %}
<div class="movie-tagline">"{{ tagline }}"</div>
{% endif %}{% endif %}
{% endif %}
<div style="margin-top:0.75rem">
<a href="/reviews/new" class="btn-small">+ Log a review</a>
{% if ctx.user_id.is_some() %}
{% if on_watchlist %}
<form method="post" action="/watchlist/{{ movie.id().value() }}/remove" style="display:inline">
<input type="hidden" name="redirect_after" value="/movies/{{ movie.id().value() }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit" class="btn-small" style="color:#4aaa77;border-color:rgba(74,170,119,.3)">✓ On watchlist · Remove</button>
</form>
{% else %}
<form method="post" action="/watchlist/add" style="display:inline">
<input type="hidden" name="movie_id" value="{{ movie.id().value() }}">
<input type="hidden" name="redirect_after" value="/movies/{{ movie.id().value() }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit" class="btn-small">+ Want to watch</button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</article>
{# ── Stats ── #}
<div class="stats-bar">
{% if let Some(avg) = stats.avg_rating %}
<div class="stat-box">
<div class="stat-value">{{ format!("{:.1}", avg) }}★</div>
<div class="stat-label">avg rating</div>
</div>
{% endif %}
<div class="stat-box">
<div class="stat-value">{{ stats.total_count }}</div>
<div class="stat-label">reviews</div>
</div>
{% if stats.federated_count > 0 %}
<div class="stat-box">
<div class="stat-value">{{ stats.federated_count }}</div>
<div class="stat-label">federated</div>
</div>
{% endif %}
<div class="stat-box histogram">
<div class="stat-label">distribution</div>
<div class="histogram-row"><span class="hist-label">5★</span><div class="hist-bar-wrap"><div class="hist-bar" style="width:{% if histogram_max > 0 %}{{ stats.rating_histogram[4] * 100 / histogram_max }}{% else %}0{% endif %}%"></div></div><span class="hist-count">{{ stats.rating_histogram[4] }}</span></div>
<div class="histogram-row"><span class="hist-label">4★</span><div class="hist-bar-wrap"><div class="hist-bar" style="width:{% if histogram_max > 0 %}{{ stats.rating_histogram[3] * 100 / histogram_max }}{% else %}0{% endif %}%"></div></div><span class="hist-count">{{ stats.rating_histogram[3] }}</span></div>
<div class="histogram-row"><span class="hist-label">3★</span><div class="hist-bar-wrap"><div class="hist-bar" style="width:{% if histogram_max > 0 %}{{ stats.rating_histogram[2] * 100 / histogram_max }}{% else %}0{% endif %}%"></div></div><span class="hist-count">{{ stats.rating_histogram[2] }}</span></div>
<div class="histogram-row"><span class="hist-label">2★</span><div class="hist-bar-wrap"><div class="hist-bar" style="width:{% if histogram_max > 0 %}{{ stats.rating_histogram[1] * 100 / histogram_max }}{% else %}0{% endif %}%"></div></div><span class="hist-count">{{ stats.rating_histogram[1] }}</span></div>
<div class="histogram-row"><span class="hist-label">1★</span><div class="hist-bar-wrap"><div class="hist-bar" style="width:{% if histogram_max > 0 %}{{ stats.rating_histogram[0] * 100 / histogram_max }}{% else %}0{% endif %}%"></div></div><span class="hist-count">{{ stats.rating_histogram[0] }}</span></div>
</div>
</div>
{% if let Some(p) = profile %}
{# ── Overview ── #}
{% if let Some(overview) = &p.overview %}{% if !overview.is_empty() %}
<p class="movie-overview">{{ overview }}</p>
{% endif %}{% endif %}
{# ── Cast ── #}
{% if !p.cast.is_empty() %}
<div class="feed-section-label">CAST</div>
<div class="cast-strip">
{% for (i, member) in p.cast.iter().enumerate() %}{% if i < 10 %}
<div class="cast-card">
{% if let Some(path) = &member.profile_path %}
<img src="https://image.tmdb.org/t/p/w185{{ path }}" alt="{{ member.name }}" loading="lazy">
{% else %}
<img src="/static/person-placeholder.svg" alt="{{ member.name }}">
{% endif %}
<div class="cast-name">{{ member.name }}</div>
<div class="cast-char">{{ member.character }}</div>
</div>
{% endif %}{% endfor %}
</div>
{% endif %}
{# ── Crew ── #}
{% if !p.crew.is_empty() %}
<div class="feed-section-label">CREW</div>
<ul class="crew-list">
{% for member in &p.crew %}
{% if member.job == "Screenplay" || member.job == "Story" || member.job == "Original Music Composer" || member.job == "Director of Photography" %}
<li><span class="crew-role">{{ member.job }}</span>{{ member.name }}</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% endif %}
{# ── Reviews ── #}
<div class="feed-section-label">REVIEWS</div>
<div class="diary">
{% for entry in reviews %}
<article class="entry review-card {% if ctx.is_current_user(entry.review().user_id().value()) %}review-own{% endif %} {% if entry.review().is_remote() %}review-federated{% endif %}">
<div class="entry-body">
<div class="rating">
{% for filled in entry.review().stars() %}
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %}
</div>
{% if let Some(comment) = entry.review().comment() %}
{% if entry.review().spoilers() %}
<details class="comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }}
</details>
{% else %}
<div class="comment">{{ comment.value() }}</div>
{% endif %}
{% endif %}
<div class="feed-meta">
{% match entry.review().source() %}
{% when ReviewSource::Remote with { actor_url } %}
<a href="{{ actor_url }}" class="feed-user" target="_blank" rel="noopener noreferrer">{{ entry.user_display_name() }}</a>
<span class="feed-time">{{ entry.review().watched_at().format("%b %-d, %Y") }}</span>
<span class="remote-badge">&#8599; federated</span>
{% when ReviewSource::Local %}
{% if ctx.is_current_user(entry.review().user_id().value()) %}
<span class="feed-user">you</span>
{% else %}
<a href="/users/{{ entry.review().user_id().value() }}" class="feed-user">{{ entry.user_display_name() }}</a>
{% endif %}
<span class="feed-time">{{ entry.review().watched_at().format("%b %-d, %Y") }}</span>
{% endmatch %}
</div>
{% if ctx.is_current_user(entry.review().user_id().value()) %}
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
<input type="hidden" name="redirect_after" value="/movies/{{ movie.id().value() }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Delete</button>
</form>
{% endif %}
</div>
</article>
{% else %}
<p class="empty">No reviews yet.</p>
{% endfor %}
</div>
<nav class="pagination">
{% if current_offset >= limit %}
<a href="/movies/{{ movie.id().value() }}?offset={{ current_offset - limit }}&limit={{ limit }}" class="page-nav">&larr; Prev</a>
{% endif %}
{% if has_more %}
<a href="/movies/{{ movie.id().value() }}?offset={{ current_offset + limit }}&limit={{ limit }}" class="page-nav">Next &rarr;</a>
{% endif %}
</nav>
</div>
{% endblock %}

View File

@@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block content %}
<h1>Log a Review</h1>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
<form method="POST" action="/reviews">
<label>
OMDB ID <span class="optional">(optional)</span><br>
<input type="text" name="external_metadata_id" placeholder="tt0166924">
</label>
<hr>
<label>
Title<br>
<input type="text" name="manual_title">
</label>
<label>
Year<br>
<input type="number" name="manual_release_year" min="1888" max="2100">
</label>
<label>
Director<br>
<input type="text" name="manual_director">
</label>
<hr>
<label>
Rating (0–5)<br>
<input type="number" name="rating" min="0" max="5" required>
</label>
<label>
Watched<br>
<input type="date" name="watched_at" required>
</label>
<label>
Comment<br>
<textarea name="comment"></textarea>
</label>
<label>
Contains spoilers <input type="checkbox" name="spoilers" value="true">
</label>
<label>
Watched via <span class="optional">(optional)</span><br>
<select name="watch_medium">
<option value="">—</option>
<option value="cinema">Cinema</option>
<option value="streaming">Streaming</option>
<option value="tv">TV</option>
<option value="physical_media">Physical Media</option>
<option value="download">Download</option>
<option value="media_server">Media Server</option>
<option value="other">Other</option>
</select>
</label>
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Log Review</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,282 @@
{% extends "base.html" %}
{% block content %}
<div class="profile">
<div class="stats-header">
<div class="profile-name">{{ profile_display_name }}</div>
<div class="stats-grid">
<div class="stat-tile">
<div class="stat-value">{{ stats.total_movies }}</div>
<div class="stat-label">movies</div>
</div>
<div class="stat-tile">
<div class="stat-value">{{ avg_rating_display }}★</div>
<div class="stat-label">avg rating</div>
</div>
<div class="stat-tile">
<div class="stat-value">{{ favorite_director_display }}</div>
<div class="stat-label">fav director</div>
</div>
<div class="stat-tile">
<div class="stat-value">{{ most_active_month_display }}</div>
<div class="stat-label">most active</div>
</div>
</div>
</div>
{% if !goals.is_empty() %}
<div class="goals-section">
{% for g in goals %}
<div class="goal-card">
<div class="goal-header">
<span class="goal-label">{{ g.year }} Goal</span>
<span class="goal-count">{{ g.current_count }}&thinsp;/&thinsp;{{ g.target_count }} movies</span>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: {{ g.percentage }}%"></div>
</div>
{% if g.is_complete %}
<span class="goal-complete">✦ Goal reached!</span>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% if is_own_profile %}
<section class="follow-section">
<h3>Follow remote user</h3>
<form method="POST" action="/users/{{ profile_user_id }}/follow">
<input type="text" name="handle" placeholder="user@instance.example.com" required>
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Follow</button>
</form>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
</section>
<a href="/users/{{ profile_user_id }}/following-list">View following ({{ following_count }})</a>
<a href="/users/{{ profile_user_id }}/followers-list">View followers ({{ followers_count }})</a>
{% if !pending_followers.is_empty() %}
<section class="pending-followers">
<h3>Pending follow requests ({{ pending_followers.len() }})</h3>
<ul class="pending-list">
{% for actor in pending_followers %}
<li class="pending-item">
<span class="pending-handle"><strong>{{ actor.handle }}</strong></span>
<a href="{{ actor.url }}" class="pending-url" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
<form method="POST" action="/users/{{ profile_user_id }}/followers/accept" class="inline-form">
<input type="hidden" name="actor_url" value="{{ actor.url }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit" class="btn-accept">Accept</button>
</form>
<form method="POST" action="/users/{{ profile_user_id }}/followers/reject" class="inline-form">
<input type="hidden" name="actor_url" value="{{ actor.url }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit" class="btn-reject">Reject</button>
</form>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
<section class="export-section">
<h3>Export diary</h3>
<a href="/diary/export?format=csv">Download CSV</a>
<a href="/diary/export?format=json">Download JSON</a>
</section>
<section class="profile-manage">
<h3>Account</h3>
<a href="/users/{{ profile_user_id }}/watchlist">Watchlist</a>
<a href="/settings/profile">Profile settings</a>
<a href="/settings/integrations">Integrations</a>
<a href="/social/blocked">Blocked users</a>
{% if ctx.is_admin %}
<a href="/admin/blocked-domains">Admin — blocked domains</a>
{% endif %}
</section>
{% endif %}
{% if ctx.is_admin && !is_own_profile && !is_service_account %}
<section class="profile-manage">
<h3>Admin</h3>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
<p style="font-size:.85em;opacity:.7">This user and all their data will be permanently deleted, fediverse servers are notified, and the username is retired for good. Enter your own password to confirm.</p>
<form method="POST" action="/users/{{ profile_user_id }}/delete">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<input type="password" name="password" placeholder="Your password" required autocomplete="current-password">
<button type="submit" class="btn-small" style="color:#e57a7a;border-color:rgba(229,122,122,.3)">Delete user</button>
</form>
</section>
{% endif %}
<div class="view-tabs">
<a href="?view=recent" class="view-tab {% if view == "recent" %}active{% endif %}">Recent</a>
<a href="?view=ratings" class="view-tab {% if view == "ratings" %}active{% endif %}">Top Rated</a>
<a href="?view=history" class="view-tab {% if view == "history" %}active{% endif %}">History</a>
<a href="?view=trends" class="view-tab {% if view == "trends" %}active{% endif %}">Trends</a>
</div>
{% if view == "recent" || view == "ratings" %}
<form method="get" class="feed-filters" action="/users/{{ profile_user_id }}">
<input type="hidden" name="view" value="{{ view }}">
<input type="hidden" name="limit" value="{{ limit }}">
{% include "_filter_controls.html" %}
{% if sort_by != "date" || !search.is_empty() %}
<a href="/users/{{ profile_user_id }}?view={{ view }}" class="clear-filters">Clear</a>
{% endif %}
</form>
{% endif %}
{% if view == "history" %}
{% if let Some(hist) = history %}
<div class="heatmap-section">
<div class="heatmap-label">Movies watched this year</div>
<div class="heatmap">
{% for cell in heatmap %}
<div class="heatmap-cell" style="--alpha: {{ cell.alpha }}">
<div class="heatmap-count">{{ cell.count }}</div>
<div class="heatmap-month">{{ cell.month_label }}</div>
</div>
{% endfor %}
</div>
</div>
{% for month in hist %}
<div class="history-month">
<h3 class="month-heading">{{ month.month_label }} <span class="month-count">{{ month.count }}</span></h3>
<div class="diary">
{% for entry in month.entries %}
<article class="entry">
{% if let Some(poster) = entry.movie().poster_path() %}
<div class="poster"><img src="{{ poster.value()|poster_src }}" alt=""></div>
{% endif %}
<div class="entry-body">
<div class="entry-title"><a href="/movies/{{ entry.movie().id().value() }}" class="movie-title-link">{{ entry.movie().title().value() }}</a> <span class="year">({{ entry.movie().release_year().value() }})</span></div>
{% if let Some(dir) = entry.movie().director() %}<div class="director">{{ dir }}</div>{% endif %}
<div class="rating">
{% for filled in entry.review().stars() %}
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %}
</div>
<div class="watched-at">{{ entry.review().watched_at().format("%b %-d") }}</div>
</div>
</article>
{% endfor %}
</div>
</div>
{% else %}
<p class="empty">No movies logged yet.</p>
{% endfor %}
{% endif %}
{% elif view == "trends" %}
{% if let Some(t) = trends %}
<div class="trends-section">
{% if !monthly_rating_rows.is_empty() %}
<div class="chart-block">
<div class="chart-label">Average rating per month</div>
<div class="bar-chart">
{% for row in monthly_rating_rows %}
<div class="bar-col">
<div class="bar-value">{{ "{:.1}"|format(row.rating.avg_rating) }}</div>
<div class="bar-fill" style="height: {{ row.bar_height_px }}px"></div>
<div class="bar-month">{{ row.rating.month_label }}</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if !t.top_directors.is_empty() %}
<div class="chart-block">
<div class="chart-label">Most watched directors</div>
<div class="director-chart">
{% for d in t.top_directors %}
<div class="director-row">
<div class="director-name">{{ d.director }}</div>
<div class="director-bar">
{% if t.max_director_count > 0 %}
<div class="director-bar-fill" style="width: {{ d.count * 100 / t.max_director_count }}%"></div>
{% else %}
<div class="director-bar-fill" style="width: 0%"></div>
{% endif %}
</div>
<div class="director-count">{{ d.count }}</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}
{% else %}
{% if let Some(paged) = entries %}
<div class="diary">
{% for entry in paged.items %}
<article class="entry">
{% if let Some(poster) = entry.movie().poster_path() %}
<div class="poster">
<img src="{{ poster.value()|poster_src }}" alt="">
</div>
{% endif %}
<div class="entry-body">
<div class="entry-title">
<a href="/movies/{{ entry.movie().id().value() }}" class="movie-title-link">{{ entry.movie().title().value() }}</a>
<span class="year">({{ entry.movie().release_year().value() }})</span>
</div>
{% if let Some(dir) = entry.movie().director() %}
<div class="director">{{ dir }}</div>
{% endif %}
<div class="rating">
{% for filled in entry.review().stars() %}
<span class="star {% if filled %}filled{% else %}empty{% endif %}">★</span>
{% endfor %}
</div>
{% if let Some(comment) = entry.review().comment() %}
{% if entry.review().spoilers() %}
<details class="comment spoiler">
<summary style="cursor:pointer;opacity:.75">Contains spoilers</summary>
{{ comment.value() }}
</details>
{% else %}
<div class="comment">{{ comment.value() }}</div>
{% endif %}
{% endif %}
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
{% if ctx.is_current_user(entry.review().user_id().value()) %}
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
<input type="hidden" name="redirect_after" value="/users/{{ profile_user_id }}?offset={{ current_offset }}{{ self.filter_qs() }}">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Delete</button>
</form>
{% endif %}
</div>
</article>
{% else %}
<p class="empty">No reviews yet.</p>
{% endfor %}
</div>
<nav class="pagination">
{% if current_offset >= limit %}
<a href="/users/{{ profile_user_id }}?offset={{ current_offset - limit }}{{ self.filter_qs() }}" class="page-nav">&larr; Prev</a>
{% endif %}
{% for item in page_items %}
{% if item.is_ellipsis %}
<span class="page-ellipsis">&hellip;</span>
{% elif item.is_current %}
<span class="page-num current">{{ item.number + 1 }}</span>
{% else %}
<a href="/users/{{ profile_user_id }}?offset={{ item.number * limit }}{{ self.filter_qs() }}" class="page-num">{{ item.number + 1 }}</a>
{% endif %}
{% endfor %}
{% if has_more %}
<a href="/users/{{ profile_user_id }}?offset={{ current_offset + limit }}{{ self.filter_qs() }}" class="page-nav">Next &rarr;</a>
{% endif %}
</nav>
{% endif %}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block content %}
<h1>Profile Settings</h1>
<p style="font-size:.85em;opacity:.7;margin-bottom:1rem">
<a href="/settings/integrations">Integrations (Jellyfin/Plex)</a>
</p>
{% if saved %}
<p class="success">Saved.</p>
{% endif %}
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
<form method="post" action="/settings/profile" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<label>
Bio<br>
<textarea name="bio">{% if let Some(b) = bio %}{{ b }}{% endif %}</textarea>
</label>
<label>
Also known as (actor URL for account migration)<br>
<input type="text" name="also_known_as" value="{% if let Some(v) = also_known_as %}{{ v }}{% endif %}">
</label>
{% if let Some(url) = avatar_url %}
<div>
<p>Current avatar:</p>
<img src="{{ url }}" alt="Current avatar" style="max-width:128px;max-height:128px;">
</div>
{% endif %}
<label>
Avatar image<br>
<input type="file" name="avatar" accept="image/jpeg,image/png,image/webp">
</label>
{% if let Some(url) = banner_url %}
<div>
<p>Current banner:</p>
<img src="{{ url }}" alt="Current banner" style="max-width:600px;max-height:200px;">
</div>
{% endif %}
<label>
Banner image<br>
<input type="file" name="banner" accept="image/jpeg,image/png,image/webp">
</label>
<fieldset>
<legend>Profile fields (max 4)</legend>
{% for i in 0..4usize %}
<div>
<input type="text" name="field_name_{{ i }}" placeholder="Label"
value="{% if let Some((n, _)) = profile_fields.get(*i) %}{{ n }}{% endif %}">
<input type="text" name="field_value_{{ i }}" placeholder="Value"
value="{% if let Some((_, v)) = profile_fields.get(*i) %}{{ v }}{% endif %}">
</div>
{% endfor %}
</fieldset>
<button type="submit">Save</button>
</form>
<section style="margin-top:2rem;padding-top:1rem;border-top:1px solid rgba(255,255,255,0.1)">
<h3>Embed widget</h3>
<p style="font-size:.85em;opacity:.7;margin-bottom:.5rem">Add your diary to your blog or website:</p>
<pre style="background:rgba(255,255,255,0.06);padding:12px;border-radius:8px;font-size:.8em;overflow-x:auto;white-space:pre-wrap;word-break:break-all"><code>&lt;iframe src="{{ embed_url }}" width="100%" height="600" style="border:none;border-radius:8px" loading="lazy"&gt;&lt;/iframe&gt;</code></pre>
</section>
<section style="margin-top:2rem;padding-top:1rem;border-top:1px solid rgba(255,255,255,0.1)">
<h3>Delete account</h3>
<p style="font-size:.85em;opacity:.7;margin-bottom:.5rem">
Deleting your account is permanent and cannot be undone. Your reviews, watchlist, goals and follows are removed, fediverse servers are notified, and your username can never be reused.
</p>
<form method="post" action="/settings/account/delete">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<label>
Confirm with your password<br>
<input type="password" name="password" required autocomplete="current-password">
</label>
<button type="submit" class="btn-small" style="color:#e57a7a;border-color:rgba(229,122,122,.3)">Delete my account</button>
</form>
</section>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
{% if let Some(err) = error %}
<p class="error">{{ err }}</p>
{% endif %}
<form method="POST" action="/register">
<label>
Username<br>
<small>2–30 chars: letters, digits, underscores, hyphens. This becomes your ActivityPub handle.</small><br>
<input type="text" name="username" required autocomplete="username"
pattern="[a-z0-9_\-]{2,30}" title="2–30 lowercase letters, digits, underscores or hyphens">
</label>
<label>
Email<br>
<input type="email" name="email" required autocomplete="email">
</label>
<label>
Password<br>
<input type="password" name="password" required autocomplete="new-password">
</label>
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit">Register</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block content %}
<div class="users-list">
<h2 class="page-title">Members</h2>
{% if let Some(viewer_id) = ctx.user_id %}
<form method="POST" action="/users/{{ viewer_id }}/follow" class="follow-form">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<input type="hidden" name="redirect_after" value="/users">
<input type="text" name="handle" placeholder="@user@instance.tld" required>
<button type="submit">Follow</button>
</form>
{% endif %}
{% for user in users %}
<div class="user-row">
{% if let Some(url) = &user.avatar_url %}
<img class="user-avatar user-avatar-img" src="{{ url }}" alt="{{ user.initial }}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;">
{% else %}
<div class="user-avatar">{{ user.initial }}</div>
{% endif %}
<div class="user-info">
<div class="user-name">{{ user.display_name }}</div>
<div class="user-meta">{{ user.total_movies }} movies · avg {{ user.avg_rating_display }}★</div>
</div>
<a href="/users/{{ user.user_id }}" class="btn-secondary">View profile →</a>
</div>
{% else %}
<p class="empty">No users yet.</p>
{% endfor %}
{% if !remote_actors.is_empty() %}
<h2 class="page-title federated-title">Federated</h2>
{% for actor in remote_actors %}
<div class="user-row">
<div class="user-avatar federated-avatar">{{ actor.initial }}</div>
<div class="user-info">
<div class="user-name">{{ actor.display_name }}</div>
<div class="user-meta muted">{{ actor.handle }}</div>
</div>
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer" class="btn-secondary">
View profile ↗
</a>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block content %}
<div class="movie-detail">
<div class="entry-title" style="margin-bottom:1rem">Watch Queue</div>
{% if let Some(err) = error %}
<p class="form-error">{{ err }}</p>
{% endif %}
{% if entries.is_empty() %}
<p class="empty">
No pending watches.
<a href="/settings/integrations">Connect Jellyfin</a> to start auto-logging.
</p>
{% else %}
<p style="opacity:.7;font-size:.85em;margin-bottom:1rem">
Movies you watched via Jellyfin. Rate and confirm to add to your diary, or dismiss.
</p>
<div class="diary">
{% for entry in entries %}
<article class="entry">
<div class="entry-body">
<div class="entry-title">
{% if let Some(url) = &entry.movie_url %}
<a href="{{ url }}" class="movie-title-link">{{ entry.title }}</a>
{% else %}
{{ entry.title }}
{% endif %}
{% if let Some(y) = entry.year %}
<span class="year">({{ y }})</span>
{% endif %}
</div>
<div class="feed-meta" style="margin-top:0.3rem">
<span class="feed-time">Watched {{ entry.watched_at }}</span>
<span class="feed-time" style="margin-left:0.5rem;opacity:.6">via {{ entry.source }}</span>
</div>
<form method="post" action="/watch-queue/{{ entry.id }}/confirm" style="margin-top:0.6rem;display:flex;gap:0.5rem;flex-wrap:wrap;align-items:flex-end">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<div>
<label style="display:block;font-size:0.8em;opacity:.6;margin-bottom:0.25rem">Rating</label>
<select name="rating" style="width:auto">
<option value="0">—</option>
<option value="1">1★</option>
<option value="2">2★</option>
<option value="3">3★</option>
<option value="4">4★</option>
<option value="5">5★</option>
</select>
</div>
<div style="flex:1;min-width:120px">
<label style="display:block;font-size:0.8em;opacity:.6;margin-bottom:0.25rem">Comment</label>
<input type="text" name="comment" placeholder="Optional" style="width:100%;box-sizing:border-box">
</div>
<button type="submit" class="btn-small" style="height:2.25rem">Confirm</button>
</form>
<form method="post" action="/watch-queue/{{ entry.id }}/dismiss" style="margin-top:0.3rem">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit" class="btn-small" style="color:#e57a7a;border-color:rgba(229,122,122,.3);font-size:.8em">Dismiss</button>
</form>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block content %}
<div class="movie-detail">
<div class="entry-title" style="margin-bottom:1rem">Watchlist</div>
{% if is_owner %}
{% if let Some(err) = &error %}
<p class="form-error">{{ err }}</p>
{% endif %}
<form method="post" action="/watchlist/add" style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:1.5rem;align-items:flex-end">
<div style="flex:1;min-width:200px">
<label style="display:block;font-size:0.8em;opacity:.6;margin-bottom:0.25rem">Title or TMDB ID</label>
<input type="text" name="query" placeholder='e.g. "Dune" or tmdb:438631' style="width:100%;box-sizing:border-box">
</div>
<div style="width:90px">
<label style="display:block;font-size:0.8em;opacity:.6;margin-bottom:0.25rem">Year</label>
<input type="number" name="year" placeholder="2021" min="1888" max="2099" style="width:100%;box-sizing:border-box">
</div>
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<input type="hidden" name="redirect_after" value="/users/{{ owner_id }}/watchlist">
<button type="submit" class="btn-small" style="height:2.25rem">Add</button>
</form>
{% endif %}
{% if display_entries.is_empty() %}
<p class="empty">No movies on the watchlist yet.</p>
{% else %}
<div class="diary">
{% for entry in display_entries %}
<article class="entry">
{% if let Some(url) = &entry.poster_url %}
<div class="poster"><img src="{{ url }}" alt=""></div>
{% endif %}
<div class="entry-body">
<div class="entry-title">
{% if let Some(url) = &entry.movie_url %}
<a href="{{ url }}" class="movie-title-link">{{ entry.movie_title }}</a>
{% else %}
{{ entry.movie_title }}
{% endif %}
<span class="year">({{ entry.release_year }})</span>
</div>
<div class="feed-meta" style="margin-top:0.4rem">
<span class="feed-time">Added {{ entry.added_at }}</span>
</div>
{% if let Some(remove_url) = &entry.remove_url %}
<form method="post" action="{{ remove_url }}" style="margin-top:0.5rem">
<input type="hidden" name="redirect_after" value="/users/{{ owner_id }}/watchlist">
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
<button type="submit" class="btn-small" style="color:#e57a7a;border-color:rgba(229,122,122,.3)">Remove</button>
</form>
{% endif %}
</div>
</article>
{% endfor %}
</div>
<nav class="pagination">
{% if current_offset >= limit %}
<a href="/users/{{ owner_id }}/watchlist?offset={{ current_offset - limit }}&limit={{ limit }}" class="page-nav">&larr; Prev</a>
{% endif %}
{% if has_more %}
<a href="/users/{{ owner_id }}/watchlist?offset={{ current_offset + limit }}&limit={{ limit }}" class="page-nav">Next &rarr;</a>
{% endif %}
</nav>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,246 @@
{% extends "base.html" %}
{% block content %}
<div class="wu-container">
<section class="wu-section wu-hero">
<span class="wu-eyebrow">Your Year in Movies</span>
<h1 class="wu-year">{{ year_label }}</h1>
<div class="wu-big-number">{{ report.total_movies }}</div>
<div class="wu-subtitle">movies watched</div>
{% if report.total_watch_time_minutes > 0 %}
<div class="wu-detail">{{ watch_time_display }} of watch time</div>
{% endif %}
</section>
<section class="wu-section">
<div class="wu-card">
<h2>Ratings</h2>
{% if let Some(avg) = report.avg_rating %}
<div class="wu-big-number" style="font-size:3.5rem">{{ avg|fmt("{:.1}") }}★</div>
<div class="wu-subtitle">average rating</div>
{% endif %}
<div class="wu-rating-bars">
{% for i in 0..5 %}
{% let ri = 4 - i %}
<div class="wu-rating-row">
<span class="wu-star-label">{{ ri + 1 }}★</span>
<div class="wu-bar-track">
<div class="wu-bar" style="width: {{ rating_pcts[ri] }}%"></div>
</div>
<span class="wu-bar-count">{{ report.rating_distribution[ri] }}</span>
</div>
{% endfor %}
</div>
<div class="wu-stat-pills">
{% if let Some(month) = report.busiest_month %}
<div class="wu-pill"><span class="wu-pill-label">Busiest month</span><strong>{{ month }}</strong></div>
{% endif %}
{% if let Some(day) = report.busiest_day_of_week %}
<div class="wu-pill"><span class="wu-pill-label">Favorite day</span><strong>{{ day }}</strong></div>
{% endif %}
</div>
</div>
</section>
{% if !report.top_directors.is_empty() %}
<section class="wu-section">
<div class="wu-card">
<h2>Top Directors</h2>
<div class="wu-diversity">{{ report.director_diversity }} unique directors</div>
<ol class="wu-rank-list">
{% for d in report.top_directors.iter().take(5) %}
<li class="wu-rank-item">
<span class="wu-rank-num">{{ loop.index }}</span>
<div class="wu-rank-info">
<span class="wu-rank-name">{{ d.name }}</span>
<span class="wu-rank-meta">{{ d.count }} films · {{ d.avg_rating|fmt("{:.1}") }}★</span>
</div>
</li>
{% endfor %}
</ol>
</div>
</section>
{% endif %}
{% if !report.top_actors.is_empty() %}
<section class="wu-section">
<div class="wu-card">
<h2>Top Actors</h2>
<div class="wu-diversity">{{ report.actor_diversity }} unique actors</div>
<ol class="wu-rank-list">
{% for a in report.top_actors.iter().take(5) %}
<li class="wu-rank-item">
<span class="wu-rank-num">{{ loop.index }}</span>
<div class="wu-rank-info">
<span class="wu-rank-name">{{ a.name }}</span>
<span class="wu-rank-meta">{{ a.count }} films · {{ a.avg_rating|fmt("{:.1}") }}★</span>
</div>
</li>
{% endfor %}
</ol>
</div>
</section>
{% endif %}
{% if !report.top_genres.is_empty() %}
<section class="wu-section">
<div class="wu-card">
<h2>Genres</h2>
<div class="wu-diversity">{{ report.genre_diversity }} genres explored</div>
<div class="wu-genre-bars">
{% for g in report.top_genres.iter().take(8).enumerate() %}
<div class="wu-genre-row">
<span class="wu-genre-name">{{ g.1.genre }}</span>
<div class="wu-bar-track">
<div class="wu-bar" style="width: {{ genre_pcts[g.0] }}%"></div>
</div>
<span class="wu-genre-count">{{ g.1.count }}</span>
</div>
{% endfor %}
</div>
<div class="wu-stat-pills">
{% if let Some(best) = report.highest_rated_genre %}
<div class="wu-pill"><span class="wu-pill-label">Highest rated</span><strong>{{ best }}</strong></div>
{% endif %}
{% if let Some(worst) = report.lowest_rated_genre %}
<div class="wu-pill"><span class="wu-pill-label">Lowest rated</span><strong>{{ worst }}</strong></div>
{% endif %}
</div>
</div>
</section>
{% endif %}
<section class="wu-section">
<div class="wu-card wu-card-wide">
<h2>Highlights</h2>
<div class="wu-highlight-grid">
{% if let Some(m) = report.highest_rated_movie %}
<div class="wu-highlight-card">
{% if let Some(p) = m.poster_path %}
<img src="{{ p|poster_src }}" class="wu-highlight-poster" alt="{{ m.title }}">
{% endif %}
<div class="wu-highlight-body">
<div class="wu-highlight-label">Highest Rated</div>
<div class="wu-highlight-title">{{ m.title }}</div>
<div class="wu-highlight-year">{{ m.year }}</div>
</div>
</div>
{% endif %}
{% if let Some(m) = report.lowest_rated_movie %}
<div class="wu-highlight-card">
{% if let Some(p) = m.poster_path %}
<img src="{{ p|poster_src }}" class="wu-highlight-poster" alt="{{ m.title }}">
{% endif %}
<div class="wu-highlight-body">
<div class="wu-highlight-label">Lowest Rated</div>
<div class="wu-highlight-title">{{ m.title }}</div>
<div class="wu-highlight-year">{{ m.year }}</div>
</div>
</div>
{% endif %}
{% if let Some(m) = report.oldest_movie %}
<div class="wu-highlight-card">
{% if let Some(p) = m.poster_path %}
<img src="{{ p|poster_src }}" class="wu-highlight-poster" alt="{{ m.title }}">
{% endif %}
<div class="wu-highlight-body">
<div class="wu-highlight-label">Oldest Film</div>
<div class="wu-highlight-title">{{ m.title }}</div>
<div class="wu-highlight-year">{{ m.year }}</div>
</div>
</div>
{% endif %}
{% if let Some(m) = report.newest_movie %}
<div class="wu-highlight-card">
{% if let Some(p) = m.poster_path %}
<img src="{{ p|poster_src }}" class="wu-highlight-poster" alt="{{ m.title }}">
{% endif %}
<div class="wu-highlight-body">
<div class="wu-highlight-label">Newest Film</div>
<div class="wu-highlight-title">{{ m.title }}</div>
<div class="wu-highlight-year">{{ m.year }}</div>
</div>
</div>
{% endif %}
{% if let Some(m) = report.longest_movie %}
<div class="wu-highlight-card">
{% if let Some(p) = m.poster_path %}
<img src="{{ p|poster_src }}" class="wu-highlight-poster" alt="{{ m.title }}">
{% endif %}
<div class="wu-highlight-body">
<div class="wu-highlight-label">Longest</div>
<div class="wu-highlight-title">{{ m.title }}</div>
{% if let Some(rt) = m.runtime_minutes %}
<div class="wu-highlight-year">{{ rt }} min</div>
{% endif %}
</div>
</div>
{% endif %}
{% if let Some(m) = report.shortest_movie %}
<div class="wu-highlight-card">
{% if let Some(p) = m.poster_path %}
<img src="{{ p|poster_src }}" class="wu-highlight-poster" alt="{{ m.title }}">
{% endif %}
<div class="wu-highlight-body">
<div class="wu-highlight-label">Shortest</div>
<div class="wu-highlight-title">{{ m.title }}</div>
{% if let Some(rt) = m.runtime_minutes %}
<div class="wu-highlight-year">{{ rt }} min</div>
{% endif %}
</div>
</div>
{% endif %}
{% if let Some(m) = report.first_movie_of_period %}
<div class="wu-highlight-card">
{% if let Some(p) = m.poster_path %}
<img src="{{ p|poster_src }}" class="wu-highlight-poster" alt="{{ m.title }}">
{% endif %}
<div class="wu-highlight-body">
<div class="wu-highlight-label">First Watched</div>
<div class="wu-highlight-title">{{ m.title }}</div>
<div class="wu-highlight-year">{{ m.year }}</div>
</div>
</div>
{% endif %}
{% if let Some(m) = report.last_movie_of_period %}
<div class="wu-highlight-card">
{% if let Some(p) = m.poster_path %}
<img src="{{ p|poster_src }}" class="wu-highlight-poster" alt="{{ m.title }}">
{% endif %}
<div class="wu-highlight-body">
<div class="wu-highlight-label">Last Watched</div>
<div class="wu-highlight-title">{{ m.title }}</div>
<div class="wu-highlight-year">{{ m.year }}</div>
</div>
</div>
{% endif %}
</div>
</div>
</section>
{% if report.total_rewatches > 0 %}
<section class="wu-section">
<div class="wu-card">
<h2>Rewatches</h2>
<div class="wu-big-number" style="font-size:3.5rem">{{ report.total_rewatches }}</div>
<div class="wu-subtitle">movies rewatched</div>
{% if let Some(m) = report.most_rewatched_movie %}
<div class="wu-detail" style="margin-top:1rem">Most rewatched: <strong>{{ m.title }}</strong> ({{ m.year }})</div>
{% endif %}
</div>
</section>
{% endif %}
{% if !report.poster_paths.is_empty() %}
<section class="wu-section wu-mosaic-section">
<h2>Your Year in Posters</h2>
<div class="wu-poster-mosaic">
{% for path in report.poster_paths.iter() %}
<img src="{{ path|poster_src }}" alt="" loading="lazy">
{% endfor %}
</div>
</section>
{% endif %}
</div>
{% endblock %}