[templates] Add calendar view
This commit is contained in:
106
PROJECT.org
106
PROJECT.org
@ -93,9 +93,15 @@ fetching and simple saving.
|
||||
:LOGBOOK:
|
||||
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
||||
:END:
|
||||
* Backlog [22/42]
|
||||
** TODO Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
|
||||
* Backlog [26/42]
|
||||
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
|
||||
:PROPERTIES:
|
||||
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
|
||||
:END:
|
||||
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles:
|
||||
:PROPERTIES:
|
||||
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb
|
||||
:END:
|
||||
** TODO [#C] Move to using more robust mopidy-webhooks pacakge form pypi :utility:improvement:
|
||||
:PROPERTIES:
|
||||
:ID: ab31fdc3-359c-1b1d-6b9d-546b476021ba
|
||||
@ -391,16 +397,32 @@ CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
||||
}
|
||||
#+end_src
|
||||
** TODO [#C] User should be able to enable auto trail tracking via amail reader with Garmin LiveTrack URLs :vrobbler:trails:project:feature:personal:
|
||||
** TODO [#C] Allow users to see tasks on calendar view :vrobbler:personal:project:templates:feature:
|
||||
https://codepen.io/oliviale/pen/QYqybo
|
||||
** TODO [#C] Come up with a possible flow using WebDAV and super-productivity for tasks :personal:feature:project:vrobbler:tasks:
|
||||
:PROPERTIES:
|
||||
:ID: 133bcf71-078f-4efa-a029-1eae4b4d146d
|
||||
:END:
|
||||
** TODO [#B] Add importer class for IMAP imports :vrobbler:feature:imap:importers:project:personal:
|
||||
:PROPERTIES:
|
||||
:ID: b1426d92-2feb-4d15-9738-d5b7b0594f96
|
||||
:END:
|
||||
** TODO [#B] Add AllTrails as a source for Trail data :vrobbler:trails:feature:personal:project:
|
||||
:PROPERTIES:
|
||||
:ID: 39313362-cdfe-46e7-bbd4-9139a65c0b3c
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
Pretty clear, I would love to make trails more useful. Historically I wasn't
|
||||
hiking a lot, which made the source for this a bit silly. But it's clear that
|
||||
AllTrails is the best source, though having TrailForks is nice to.
|
||||
|
||||
** TODO [#B] Add `garmin_activity_id` to the TrailMetadataLog class :vrobbler:trails:feature:personal:project:
|
||||
Would be nice to have some loose connection to the actual event in my Garmin profile.
|
||||
:PROPERTIES:
|
||||
:ID: 5a4fb0f8-0555-40ec-b06f-93c26bd686f4
|
||||
:END:
|
||||
*** Description
|
||||
|
||||
Would be nice to have some loose connection to the actual event in my Garmin
|
||||
profile.
|
||||
|
||||
** TODO [#B] Explore a way to add metadata editing to scrobbles after saving :vrobbler:spike:scrobbling:personal:project:
|
||||
|
||||
Could be as simple as a JSON form on the scrobble detail page (do I have have
|
||||
@ -409,15 +431,19 @@ one of those yet?).
|
||||
** TODO [#B] Add webdav syncing to retroarch imports :vrobbler:videogames:webdav:feature:project:personal:
|
||||
** TODO [#B] Add CSV endpoint for book scrobbles that LibraryThing can ingest :personal:project:books:feature:export:
|
||||
https://app.todoist.com/app/task/add-a-csv-endpoint-for-users-book-reads-that-library-thing-can-ingest-6X7QPMRp265xMXqg#comment-6X7QrXq6gJjMP4hg
|
||||
** TODO [#B] Fix PuzzleLogData has no attribute form :vrobbler:puzzles:personal:project:logdata:
|
||||
** TODO [#B] Scrape ComicBookRoundUp ratings for comic book metadata :vrobbler:books:feature:comicbook:personal:project:
|
||||
|
||||
- Note taken on [2025-09-25 Thu 10:51]
|
||||
|
||||
As an example https://comicbookroundup.com/comic-books/reviews/humanoids-publishing/the-history-of-science-fiction
|
||||
** TODO [#B] Add PuzzleLogData class with with_people and completed :vrobbler:feature:puzzles:logdata:personal:project:
|
||||
** TODO [#A] Find page numbers for comic books from ComicVine :vrobbler:feature:books:personal:project:
|
||||
:PROPERTIES:
|
||||
:ID: 79f867c3-1288-4143-b6bf-2a452983ee9f
|
||||
:END:
|
||||
** TODO [#A] Fix koreader scrobble imports to use DST properly :vrobbler:personal:bug:books:imports:
|
||||
:PROPERTIES:
|
||||
:ID: 79758cba-a440-48b6-a637-efb88827acf2
|
||||
:END:
|
||||
|
||||
- Note taken on [2025-09-25 Thu 10:37] \\
|
||||
|
||||
@ -431,11 +457,73 @@ https://app.todoist.com/app/task/add-a-csv-endpoint-for-users-book-reads-that-li
|
||||
an hour for part of the year. Also, we'd need to adjust any old scrobbles that
|
||||
took place with DST off to roll them back by an hour.
|
||||
|
||||
|
||||
*** Description
|
||||
|
||||
This is a long-standing problem when daylight saving time takes effect. Time is
|
||||
manually set on a KoReader device (or at least, always saved in local time). So
|
||||
whatever time KoReader reports, we need to know, given the date and the user
|
||||
profile's historic timezone, how many hours to adjust the KoReader time to get
|
||||
to GMT to save it in the database.
|
||||
|
||||
** TODO [#A] When creating org-mode tasks, don't copy comments :vrobbler:bug:scrobbles:tasks:
|
||||
** TODO Fix various artist album problem with Superwolves (track with multiple artists) :vrobbler:project:music:bug:artists:
|
||||
:PROPERTIES:
|
||||
:ID: 0c762d09-fc69-4e75-be40-7eaaf04f178e
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
It would be nice to not duplicate comments that exist on a task when it's first scrobbled.
|
||||
|
||||
** TODO [#A] Fix various artist album problem with Superwolves (track with multiple artists) :vrobbler:project:music:bug:artists:
|
||||
:PROPERTIES:
|
||||
:ID: 590bc038-745f-710b-8272-4d8a3d2efa01
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
We have an issue with tracks where there are two artists, like `Matt Sweeney &
|
||||
Bonnie "Prince" Billy`
|
||||
|
||||
I think we need to allow creating a single artist with both names, so the Artist
|
||||
is the full name. That said, the ampersand is usually used to split feature
|
||||
artsits (I think) so I'm not sure how this would work reliably.
|
||||
|
||||
Also, it's possible musicbrainz does not have dual artist listings. So it's
|
||||
possible the longer term solution is to allow multiple artists per track the way
|
||||
we now allow tracks to be on multiple albums. We could deprecate the Artist FK
|
||||
or at least make it optional, and then require a M2M between Track and Artist.
|
||||
|
||||
Then this one would be a Track by both `Matt Sweeney` and `Bonnie "Prince"
|
||||
Billy`
|
||||
|
||||
** DONE [#A] Add CSS Grid calendar view for scrobbles :vrobbler:personal:project:templates:feature:
|
||||
:PROPERTIES:
|
||||
:ID: be915acf-d803-466a-8770-823819ebf2a9
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
Calendar view at /scrobbles/calendar/ showing select media types (Tasks,
|
||||
Birding, Food, Trails, VideoGames, Books) in a CSS Grid month layout.
|
||||
|
||||
- Emoji badges per scrobble on each day cell, hover reveals title
|
||||
- Click emoji to navigate to scrobble detail
|
||||
- Prev/Next month navigation
|
||||
- Based on the CodePen CSS Grid calendar pattern:
|
||||
https://codepen.io/oliviale/pen/QYqybo
|
||||
|
||||
** DONE [#C] Come up with a possible flow using WebDAV and super-productivity for tasks :personal:feature:project:vrobbler:tasks:
|
||||
:PROPERTIES:
|
||||
:ID: dd751edf-6192-2ed0-cd85-905f423f82b0
|
||||
:END:
|
||||
** DONE [#B] Fix PuzzleLogData has no attribute form :vrobbler:puzzles:personal:project:logdata:
|
||||
:PROPERTIES:
|
||||
:ID: cb8e697a-9fc2-c463-5d4d-fa15d3e833a0
|
||||
:END:
|
||||
** DONE [#B] Add PuzzleLogData class with with_people and completed :vrobbler:feature:puzzles:logdata:personal:project:
|
||||
:PROPERTIES:
|
||||
:ID: 1b38c8a8-86d3-b73d-c584-dfb0497970dc
|
||||
:END:
|
||||
** DONE Add weather lookup to the mood check-in flow :vrobbler:project:moods:feature:checkin:
|
||||
:PROPERTIES:
|
||||
:ID: 175678e2-bfe1-0833-f2a9-f03b9323db78
|
||||
|
||||
@ -5,6 +5,7 @@ from tasks.webhooks import EmacsWebhookView, TodoistWebhookView
|
||||
app_name = "scrobbles"
|
||||
|
||||
urlpatterns = [
|
||||
path("calendar/", views.ScrobbleCalendarView.as_view(), name="calendar"),
|
||||
path("search/", views.ScrobbleSearchView.as_view(), name="search"),
|
||||
path("status/", views.ScrobbleStatusView.as_view(), name="status"),
|
||||
path(
|
||||
|
||||
@ -2,6 +2,7 @@ import calendar
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
import pendulum
|
||||
import pytz
|
||||
@ -1281,6 +1282,129 @@ class EmbeddableTopArtistsWidget(TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
class ScrobbleCalendarView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "scrobbles/calendar.html"
|
||||
|
||||
CALENDAR_MEDIA_TYPES = [
|
||||
"Task",
|
||||
"BirdingLocation",
|
||||
"Food",
|
||||
"Trail",
|
||||
"VideoGame",
|
||||
"Book",
|
||||
"Mood",
|
||||
]
|
||||
|
||||
MEDIA_EMOJI = {
|
||||
"Task": "✅",
|
||||
"BirdingLocation": "🐦",
|
||||
"Food": "🍽️",
|
||||
"Trail": "🥾",
|
||||
"VideoGame": "🎮",
|
||||
"Book": "📚",
|
||||
"Mood": "🥰",
|
||||
}
|
||||
|
||||
MONTH_COLORS = [
|
||||
"#db7a7a", # Jan
|
||||
"#db847a", # Feb
|
||||
"#b0db7a", # Mar
|
||||
"#7adb82", # Apr
|
||||
"#7adbb3", # May
|
||||
"#7ab6db", # Jun
|
||||
"#7a8edb", # Jul
|
||||
"#977adb", # Aug
|
||||
"#c47adb", # Sep
|
||||
"#db7ac5", # Oct
|
||||
"#db7a90", # Nov
|
||||
"#db7a7a", # Dec
|
||||
]
|
||||
|
||||
def _day_color(self, month_index, day_num, total_days):
|
||||
import colorsys
|
||||
hue = (month_index - 1) * 30 / 360
|
||||
lightness = 0.80 + (day_num / total_days) * 0.15
|
||||
r, g, b = colorsys.hls_to_rgb(hue, lightness, 0.5)
|
||||
return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
today = timezone.localdate()
|
||||
date_param = self.request.GET.get("date", today.isoformat()[:7])
|
||||
try:
|
||||
year, month = [int(x) for x in date_param.split("-")]
|
||||
except (ValueError, IndexError):
|
||||
year, month = today.year, today.month
|
||||
|
||||
_, total_days = calendar.monthrange(year, month)
|
||||
month_start = datetime(year, month, 1).date()
|
||||
month_end = datetime(year, month, total_days).date()
|
||||
first_weekday = month_start.weekday()
|
||||
|
||||
prev_month = month_start - timedelta(days=1)
|
||||
next_month = month_end + timedelta(days=1)
|
||||
|
||||
scrobbles = (
|
||||
Scrobble.objects.filter(
|
||||
user=self.request.user,
|
||||
timestamp__date__gte=month_start,
|
||||
timestamp__date__lte=month_end,
|
||||
media_type__in=self.CALENDAR_MEDIA_TYPES,
|
||||
)
|
||||
.select_related("task", "birding_location", "food", "trail", "video_game", "book", "mood")
|
||||
.order_by("timestamp")
|
||||
)
|
||||
|
||||
day_map = {d: [] for d in range(1, total_days + 1)}
|
||||
for scrobble in scrobbles:
|
||||
day_map[scrobble.timestamp.day].append(scrobble)
|
||||
|
||||
missing_uuids = [s for s in scrobbles if not s.uuid]
|
||||
if missing_uuids:
|
||||
for scrobble in missing_uuids:
|
||||
scrobble.uuid = uuid4()
|
||||
Scrobble.objects.bulk_update(missing_uuids, ["uuid"])
|
||||
|
||||
calendar_days = []
|
||||
month_color = self.MONTH_COLORS[(month - 1) % 12]
|
||||
for day_num in range(1, total_days + 1):
|
||||
day_scrobbles = []
|
||||
for scrobble in day_map[day_num]:
|
||||
day_scrobbles.append(
|
||||
{
|
||||
"uuid": scrobble.uuid,
|
||||
"emoji": self.MEDIA_EMOJI.get(scrobble.media_type, "📌"),
|
||||
"title": str(scrobble.media_obj) if scrobble.media_obj else scrobble.media_type,
|
||||
"media_type": scrobble.media_type,
|
||||
}
|
||||
)
|
||||
calendar_days.append({
|
||||
"day": day_num,
|
||||
"scrobbles": day_scrobbles,
|
||||
"is_today": year == today.year and month == today.month and day_num == today.day,
|
||||
"color": self._day_color(month, day_num, total_days),
|
||||
})
|
||||
|
||||
ctx.update(
|
||||
{
|
||||
"year": year,
|
||||
"month": month,
|
||||
"month_name": month_start.strftime("%B"),
|
||||
"total_days": total_days,
|
||||
"first_weekday": first_weekday,
|
||||
"calendar_days": calendar_days,
|
||||
"prev_month": prev_month.isoformat()[:7],
|
||||
"next_month": next_month.isoformat()[:7],
|
||||
"today": today,
|
||||
"current_month": today.isoformat()[:7],
|
||||
"day_names": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
||||
"month_color": month_color,
|
||||
}
|
||||
)
|
||||
return ctx
|
||||
|
||||
|
||||
class ScrobbleSearchView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "scrobbles/scrobble_search.html"
|
||||
|
||||
|
||||
180
vrobbler/templates/scrobbles/calendar.html
Normal file
180
vrobbler/templates/scrobbles/calendar.html
Normal file
@ -0,0 +1,180 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
html, body { height: 100%; margin: 0; padding: 0; }
|
||||
header.navbar { display: none !important; }
|
||||
#sidebarMenu { display: none !important; }
|
||||
.container-fluid, .container-fluid > .row, .container-fluid > .row > div {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.home-btn {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 100;
|
||||
background: rgba(255,255,255,0.85);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
padding: 6px 16px;
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
color: #555;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||
width: auto;
|
||||
}
|
||||
.home-btn:hover {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
.calendar-wrapper {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
background: {{ month_color }}55;
|
||||
border: 1px solid {{ month_color }}44;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
.month-banner {
|
||||
background: {{ month_color }};
|
||||
color: rgba(255,255,255,0.7);
|
||||
font: 900 2.5rem "Montserrat", sans-serif;
|
||||
text-transform: uppercase;
|
||||
padding: 24px 20px;
|
||||
text-align: center;
|
||||
grid-column: 1 / -1;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.month-banner a {
|
||||
color: rgba(255,255,255,0.5);
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.month-banner a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.day-header {
|
||||
background: {{ month_color }};
|
||||
color: rgba(255,255,255,0.8);
|
||||
padding: 10px 4px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.day-cell {
|
||||
background: {{ month_color }}18;
|
||||
min-height: 100px;
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
}
|
||||
.day-cell--today {
|
||||
background: #fffde7;
|
||||
}
|
||||
.day-number {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #555;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.day-cell--today .day-number {
|
||||
color: #446e9b;
|
||||
background: #e8f0fe;
|
||||
display: inline-block;
|
||||
padding: 0 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.event-card {
|
||||
display: block;
|
||||
background: rgba(255,255,255,0.6);
|
||||
border-radius: 12px;
|
||||
padding: 3px 8px;
|
||||
margin: 3px 0;
|
||||
font-size: 0.8rem;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.event-card:hover {
|
||||
background: rgba(255,255,255,0.9);
|
||||
color: #000;
|
||||
}
|
||||
.calendar-footer {
|
||||
background: {{ month_color }};
|
||||
grid-column: 1 / -1;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.calendar-footer a {
|
||||
color: rgba(255,255,255,0.7);
|
||||
text-decoration: none;
|
||||
margin: 0 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.calendar-footer a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<a href="/" class="home-btn">Home</a>
|
||||
<div class="calendar-wrapper">
|
||||
<div class="calendar-grid">
|
||||
<div class="month-banner">
|
||||
<a href="?date={{ prev_month }}">←</a>
|
||||
{{ month_name }} {{ year }}
|
||||
<a href="?date={{ next_month }}">→</a>
|
||||
</div>
|
||||
|
||||
{% for name in day_names %}
|
||||
<div class="day-header">{{ name }}</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for _ in ""|ljust:first_weekday %}
|
||||
<div class="day-cell" style="background:transparent;"></div>
|
||||
{% endfor %}
|
||||
|
||||
{% for cd in calendar_days %}
|
||||
<div class="day-cell{% if cd.is_today %} day-cell--today{% endif %}"
|
||||
style="background:{% if not cd.is_today %}{{ cd.color }}{% endif %};">
|
||||
<div class="day-number">{{ cd.day }}</div>
|
||||
{% for s in cd.scrobbles %}
|
||||
<a href="{% url 'scrobbles:detail' uuid=s.uuid %}"
|
||||
class="event-card"
|
||||
title="{{ s.title }} — {{ s.media_type }}">{{ s.emoji }} {{ s.title }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="calendar-footer">
|
||||
<a href="?date={{ prev_month }}">← Previous</a>
|
||||
<a href="?date={{ current_month }}">Today</a>
|
||||
<a href="?date={{ next_month }}">Next →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -64,6 +64,9 @@
|
||||
placeholder="Search..."
|
||||
style="width: 150px;">
|
||||
</form>
|
||||
<div class="btn-group me-2">
|
||||
<a href="{% url 'scrobbles:calendar' %}" class="btn btn-sm btn-outline-secondary">Calendar</a>
|
||||
</div>
|
||||
<div class="btn-group me-2">
|
||||
<a href="{% url 'admin:index' %}" class="btn btn-sm btn-outline-secondary">Admin</a>
|
||||
</div>
|
||||
|
||||
@ -54,6 +54,9 @@
|
||||
<h1 class="h2">Search Scrobbles</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="btn-group me-2">
|
||||
<a href="{% url 'scrobbles:calendar' %}" class="btn btn-sm btn-outline-secondary">Calendar</a>
|
||||
</div>
|
||||
<div class="btn-group me-2">
|
||||
<a href="{% url 'admin:index' %}" class="btn btn-sm btn-outline-secondary">Admin</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user