[widgets] Add first run at widgets
This commit is contained in:
@ -6,14 +6,10 @@ from profiles.models import UserProfile
|
|||||||
class UserProfileForm(forms.ModelForm):
|
class UserProfileForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.request = kwargs.pop("request")
|
self.request = kwargs.pop("request")
|
||||||
self.profile = UserProfile.objects.filter(
|
self.profile = UserProfile.objects.filter(user=self.request.user).first()
|
||||||
user=self.request.user
|
|
||||||
).first()
|
|
||||||
if not self.profile:
|
if not self.profile:
|
||||||
raise Exception
|
raise Exception
|
||||||
super(UserProfileForm, self).__init__(
|
super(UserProfileForm, self).__init__(*args, instance=self.profile, **kwargs)
|
||||||
*args, **kwargs, instance=self.profile
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserProfile
|
model = UserProfile
|
||||||
@ -36,6 +32,7 @@ class UserProfileForm(forms.ModelForm):
|
|||||||
"ntfy_url",
|
"ntfy_url",
|
||||||
"ntfy_enabled",
|
"ntfy_enabled",
|
||||||
"redirect_to_webpage",
|
"redirect_to_webpage",
|
||||||
|
"enable_public_widgets",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"lastfm_password": forms.PasswordInput(render_value=True),
|
"lastfm_password": forms.PasswordInput(render_value=True),
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.29 on 2026-03-20 16:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("profiles", "0028_alter_userprofile_timezone"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userprofile",
|
||||||
|
name="enable_public_widgets",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -17,9 +17,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class UserProfile(TimeStampedModel):
|
class UserProfile(TimeStampedModel):
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
|
||||||
User, on_delete=models.CASCADE, related_name="profile"
|
|
||||||
)
|
|
||||||
timezone = models.CharField(
|
timezone = models.CharField(
|
||||||
max_length=255, choices=PRETTY_TIMEZONE_CHOICES, default="UTC"
|
max_length=255, choices=PRETTY_TIMEZONE_CHOICES, default="UTC"
|
||||||
)
|
)
|
||||||
@ -63,6 +61,8 @@ class UserProfile(TimeStampedModel):
|
|||||||
|
|
||||||
redirect_to_webpage = models.BooleanField(default=True)
|
redirect_to_webpage = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
enable_public_widgets = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"User profile for {self.user}"
|
return f"User profile for {self.user}"
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
@ -5,7 +6,7 @@ from django.views.generic import FormView
|
|||||||
from profiles.forms import UserProfileForm
|
from profiles.forms import UserProfileForm
|
||||||
|
|
||||||
|
|
||||||
class ProfileFormView(FormView):
|
class ProfileFormView(LoginRequiredMixin, FormView):
|
||||||
form_class = UserProfileForm
|
form_class = UserProfileForm
|
||||||
template_name = "profiles/settings_form.html"
|
template_name = "profiles/settings_form.html"
|
||||||
success_url = reverse_lazy("profiles:profile_settings")
|
success_url = reverse_lazy("profiles:profile_settings")
|
||||||
|
|||||||
@ -6,6 +6,26 @@ app_name = "scrobbles"
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("status/", views.ScrobbleStatusView.as_view(), name="status"),
|
path("status/", views.ScrobbleStatusView.as_view(), name="status"),
|
||||||
|
path(
|
||||||
|
"widget/top-artists/<int:user_id>/",
|
||||||
|
views.EmbeddableTopArtistWidget.as_view(),
|
||||||
|
name="embeddable-top-artists",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"widget/top-board-games/month/<int:user_id>/",
|
||||||
|
views.EmbeddableTopBoardGamesMonthWidget.as_view(),
|
||||||
|
name="embeddable-top-board-games-month",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"widget/top-board-games/week/<int:user_id>/",
|
||||||
|
views.EmbeddableTopBoardGamesWeekWidget.as_view(),
|
||||||
|
name="embeddable-top-board-games-week",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"widget/top-board-games/year/<int:user_id>/",
|
||||||
|
views.EmbeddableTopBoardGamesYearWidget.as_view(),
|
||||||
|
name="embeddable-top-board-games-year",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"manual/lookup/",
|
"manual/lookup/",
|
||||||
views.ManualScrobbleView.as_view(),
|
views.ManualScrobbleView.as_view(),
|
||||||
|
|||||||
@ -9,10 +9,11 @@ from dateutil.relativedelta import relativedelta
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||||
from django.db.models import Count, Max, Q
|
from django.db.models import Count, Max, Q
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
|
from django.http import FileResponse, Http404, HttpResponseRedirect, JsonResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -66,8 +67,11 @@ from scrobbles.utils import (
|
|||||||
get_long_plays_completed,
|
get_long_plays_completed,
|
||||||
get_long_plays_in_progress,
|
get_long_plays_in_progress,
|
||||||
)
|
)
|
||||||
|
from profiles.models import UserProfile
|
||||||
from profiles.utils import now_user_timezone
|
from profiles.utils import now_user_timezone
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -163,25 +167,17 @@ class RecentScrobbleList(ListView):
|
|||||||
next_date = date + timedelta(weeks=+2)
|
next_date = date + timedelta(weeks=+2)
|
||||||
prev_date = date + timedelta(weeks=-1)
|
prev_date = date + timedelta(weeks=-1)
|
||||||
if date.isocalendar()[1] < today.isocalendar()[1]:
|
if date.isocalendar()[1] < today.isocalendar()[1]:
|
||||||
data[
|
data["next_link"] = f"?date={next_date.strftime('%Y-W%W')}"
|
||||||
"next_link"
|
|
||||||
] = f"?date={next_date.strftime('%Y-W%W')}"
|
|
||||||
data["prev_link"] = f"?date={prev_date.strftime('%Y-W%W')}"
|
data["prev_link"] = f"?date={prev_date.strftime('%Y-W%W')}"
|
||||||
data[
|
data["title"] = f"Week {date.strftime('%-W')} of {date.year}"
|
||||||
"title"
|
|
||||||
] = f"Week {date.strftime('%-W')} of {date.year}"
|
|
||||||
data = data | Scrobble.as_dict_by_type(
|
data = data | Scrobble.as_dict_by_type(
|
||||||
Scrobble.for_week(
|
Scrobble.for_week(user_id, date.year, date.isocalendar()[1])
|
||||||
user_id, date.year, date.isocalendar()[1]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
elif date_str == "this_month" or date_str.count("-") == 1:
|
elif date_str == "this_month" or date_str.count("-") == 1:
|
||||||
next_date = date + relativedelta(months=1)
|
next_date = date + relativedelta(months=1)
|
||||||
prev_date = date + relativedelta(months=-1)
|
prev_date = date + relativedelta(months=-1)
|
||||||
if date.month < today.month:
|
if date.month < today.month:
|
||||||
data[
|
data["next_link"] = f"?date={next_date.strftime('%Y-%m')}"
|
||||||
"next_link"
|
|
||||||
] = f"?date={next_date.strftime('%Y-%m')}"
|
|
||||||
data["title"] = f"{date.strftime('%B %Y')}"
|
data["title"] = f"{date.strftime('%B %Y')}"
|
||||||
data["prev_link"] = f"?date={prev_date.strftime('%Y-%m')}"
|
data["prev_link"] = f"?date={prev_date.strftime('%Y-%m')}"
|
||||||
data = data | Scrobble.as_dict_by_type(
|
data = data | Scrobble.as_dict_by_type(
|
||||||
@ -190,24 +186,16 @@ class RecentScrobbleList(ListView):
|
|||||||
elif date_str == "today" or date_str.count("-") == 2:
|
elif date_str == "today" or date_str.count("-") == 2:
|
||||||
next_date = date + timedelta(days=1)
|
next_date = date + timedelta(days=1)
|
||||||
prev_date = date - timedelta(days=1)
|
prev_date = date - timedelta(days=1)
|
||||||
data[
|
data["prev_link"] = f"?date={prev_date.strftime('%Y-%m-%d')}"
|
||||||
"prev_link"
|
|
||||||
] = f"?date={prev_date.strftime('%Y-%m-%d')}"
|
|
||||||
if date < today:
|
if date < today:
|
||||||
data[
|
data["next_link"] = f"?date={next_date.strftime('%Y-%m-%d')}"
|
||||||
"next_link"
|
|
||||||
] = f"?date={next_date.strftime('%Y-%m-%d')}"
|
|
||||||
if date == today:
|
if date == today:
|
||||||
data["title"] = "Today"
|
data["title"] = "Today"
|
||||||
else:
|
else:
|
||||||
data["title"] = f"{date.strftime('%Y-%m-%d')}"
|
data["title"] = f"{date.strftime('%Y-%m-%d')}"
|
||||||
data[
|
data["today_link"] = f"?date={today.strftime('%Y-%m-%d')}"
|
||||||
"today_link"
|
|
||||||
] = f"?date={today.strftime('%Y-%m-%d')}"
|
|
||||||
data = data | Scrobble.as_dict_by_type(
|
data = data | Scrobble.as_dict_by_type(
|
||||||
Scrobble.for_day(
|
Scrobble.for_day(user_id, date.year, date.month, date.day)
|
||||||
user_id, date.year, date.month, date.day
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
elif date_str == "this_year" or date_str.count("-") == 0:
|
elif date_str == "this_year" or date_str.count("-") == 0:
|
||||||
next_date = date + relativedelta(years=+1)
|
next_date = date + relativedelta(years=+1)
|
||||||
@ -230,9 +218,7 @@ class RecentScrobbleList(ListView):
|
|||||||
data = data | Scrobble.as_dict_by_type(
|
data = data | Scrobble.as_dict_by_type(
|
||||||
Scrobble.for_day(user_id, date.year, date.month, date.day)
|
Scrobble.for_day(user_id, date.year, date.month, date.day)
|
||||||
)
|
)
|
||||||
data[
|
data["today_link"] = "" # f"?date={today.strftime('%Y-%m-%d')}"
|
||||||
"today_link"
|
|
||||||
] = "" # f"?date={today.strftime('%Y-%m-%d')}"
|
|
||||||
|
|
||||||
data["active_imports"] = AudioScrobblerTSVImport.objects.filter(
|
data["active_imports"] = AudioScrobblerTSVImport.objects.filter(
|
||||||
processing_started__isnull=False,
|
processing_started__isnull=False,
|
||||||
@ -261,9 +247,7 @@ class ScrobbleLongPlaysView(TemplateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context_data = super().get_context_data(**kwargs)
|
context_data = super().get_context_data(**kwargs)
|
||||||
context_data["view"] = self.request.GET.get("view", "grid")
|
context_data["view"] = self.request.GET.get("view", "grid")
|
||||||
context_data["in_progress"] = get_long_plays_in_progress(
|
context_data["in_progress"] = get_long_plays_in_progress(self.request.user)
|
||||||
self.request.user
|
|
||||||
)
|
|
||||||
context_data["completed"] = get_long_plays_completed(self.request.user)
|
context_data["completed"] = get_long_plays_completed(self.request.user)
|
||||||
return context_data
|
return context_data
|
||||||
|
|
||||||
@ -340,9 +324,7 @@ class ManualScrobbleView(FormView):
|
|||||||
scrobble_fn = MANUAL_SCROBBLE_FNS[key]
|
scrobble_fn = MANUAL_SCROBBLE_FNS[key]
|
||||||
scrobble = eval(scrobble_fn)(item_id, self.request.user.id)
|
scrobble = eval(scrobble_fn)(item_id, self.request.user.id)
|
||||||
|
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(scrobble.redirect_url(self.request.user.id))
|
||||||
scrobble.redirect_url(self.request.user.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class JsonableResponseMixin:
|
class JsonableResponseMixin:
|
||||||
@ -388,9 +370,7 @@ class AudioScrobblerImportCreateView(
|
|||||||
return HttpResponseRedirect(self.request.META.get("HTTP_REFERER"))
|
return HttpResponseRedirect(self.request.META.get("HTTP_REFERER"))
|
||||||
|
|
||||||
|
|
||||||
class KoReaderImportCreateView(
|
class KoReaderImportCreateView(LoginRequiredMixin, JsonableResponseMixin, CreateView):
|
||||||
LoginRequiredMixin, JsonableResponseMixin, CreateView
|
|
||||||
):
|
|
||||||
model = KoReaderImport
|
model = KoReaderImport
|
||||||
fields = ["sqlite_file"]
|
fields = ["sqlite_file"]
|
||||||
template_name = "scrobbles/upload_form.html"
|
template_name = "scrobbles/upload_form.html"
|
||||||
@ -449,9 +429,7 @@ def web_scrobbler_webhook(request):
|
|||||||
playing_state = "started"
|
playing_state = "started"
|
||||||
if request.POST.get("isPlaying") == "false":
|
if request.POST.get("isPlaying") == "false":
|
||||||
playing_state = "stopped"
|
playing_state = "stopped"
|
||||||
scrobble = web_scrobbler_scrobble_media(
|
scrobble = web_scrobbler_scrobble_media(youtube_id, user_id, status=playing_state)
|
||||||
youtube_id, user_id, status=playing_state
|
|
||||||
)
|
|
||||||
if not scrobble:
|
if not scrobble:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"[web_scrobbler_webhook] failed",
|
"[web_scrobbler_webhook] failed",
|
||||||
@ -469,9 +447,7 @@ def web_scrobbler_webhook(request):
|
|||||||
"[web_scrobbler_webhook] finished",
|
"[web_scrobbler_webhook] finished",
|
||||||
extra={"scrobble_id": scrobble.id},
|
extra={"scrobble_id": scrobble.id},
|
||||||
)
|
)
|
||||||
return JsonResponse(
|
return JsonResponse({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
|
||||||
{"scrobble_id": scrobble.id}, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@ -576,18 +552,12 @@ def import_audioscrobbler_file(request):
|
|||||||
scrobbles_created = []
|
scrobbles_created = []
|
||||||
# tsv_file = request.FILES[0]
|
# tsv_file = request.FILES[0]
|
||||||
|
|
||||||
file_serializer = serializers.AudioScrobblerTSVImportSerializer(
|
file_serializer = serializers.AudioScrobblerTSVImportSerializer(data=request.data)
|
||||||
data=request.data
|
|
||||||
)
|
|
||||||
if file_serializer.is_valid():
|
if file_serializer.is_valid():
|
||||||
import_file = file_serializer.save()
|
import_file = file_serializer.save()
|
||||||
return Response(
|
return Response({"scrobble_ids": scrobbles_created}, status=status.HTTP_200_OK)
|
||||||
{"scrobble_ids": scrobbles_created}, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
file_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
@ -768,9 +738,7 @@ class ChartRecordView(TemplateView):
|
|||||||
).order_by("rank")
|
).order_by("rank")
|
||||||
|
|
||||||
if charts.count() == 0:
|
if charts.count() == 0:
|
||||||
ChartRecord.build(
|
ChartRecord.build(user=self.request.user, model_str=media_type, **kwargs)
|
||||||
user=self.request.user, model_str=media_type, **kwargs
|
|
||||||
)
|
|
||||||
charts = ChartRecord.objects.filter(
|
charts = ChartRecord.objects.filter(
|
||||||
media_filter, user=self.request.user, **kwargs
|
media_filter, user=self.request.user, **kwargs
|
||||||
).order_by("rank")
|
).order_by("rank")
|
||||||
@ -802,9 +770,7 @@ class ChartRecordView(TemplateView):
|
|||||||
media_type = self.request.GET.get("media", "Track")
|
media_type = self.request.GET.get("media", "Track")
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
params = {}
|
params = {}
|
||||||
context_data["chart_type"] = self.request.GET.get(
|
context_data["chart_type"] = self.request.GET.get("chart_type", "maloja")
|
||||||
"chart_type", "maloja"
|
|
||||||
)
|
|
||||||
context_data["artist_charts"] = {}
|
context_data["artist_charts"] = {}
|
||||||
|
|
||||||
if not date:
|
if not date:
|
||||||
@ -842,9 +808,7 @@ class ChartRecordView(TemplateView):
|
|||||||
tzinfo = now.tzinfo
|
tzinfo = now.tzinfo
|
||||||
now = now.date()
|
now = now.date()
|
||||||
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
|
start_of_today = datetime.combine(now, datetime.min.time(), tzinfo)
|
||||||
start_day_of_week = start_of_today - timedelta(
|
start_day_of_week = start_of_today - timedelta(days=now.isoweekday() % 7)
|
||||||
days=now.isoweekday() % 7
|
|
||||||
)
|
|
||||||
start_day_of_month = now.replace(day=1)
|
start_day_of_month = now.replace(day=1)
|
||||||
|
|
||||||
# TV Series Scrobbles
|
# TV Series Scrobbles
|
||||||
@ -889,35 +853,25 @@ class ChartRecordView(TemplateView):
|
|||||||
context_data["tv_show_charts"] = {
|
context_data["tv_show_charts"] = {
|
||||||
"today": list(live_tv_charts(user=user, chart_period="today")),
|
"today": list(live_tv_charts(user=user, chart_period="today")),
|
||||||
"last7": list(live_tv_charts(user=user, chart_period="last7")),
|
"last7": list(live_tv_charts(user=user, chart_period="last7")),
|
||||||
"last30": list(
|
"last30": list(live_tv_charts(user=user, chart_period="last30")),
|
||||||
live_tv_charts(user=user, chart_period="last30")
|
|
||||||
),
|
|
||||||
"year": list(live_tv_charts(user=user, chart_period="year")),
|
"year": list(live_tv_charts(user=user, chart_period="year")),
|
||||||
"all": list(live_tv_charts(user=user, chart_period="all")),
|
"all": list(live_tv_charts(user=user, chart_period="all")),
|
||||||
}
|
}
|
||||||
|
|
||||||
context_data["youtube_channel_charts"] = {
|
context_data["youtube_channel_charts"] = {
|
||||||
"today": list(
|
"today": list(
|
||||||
live_youtube_channel_charts(
|
live_youtube_channel_charts(user=user, chart_period="today")
|
||||||
user=user, chart_period="today"
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
"last7": list(
|
"last7": list(
|
||||||
live_youtube_channel_charts(
|
live_youtube_channel_charts(user=user, chart_period="last7")
|
||||||
user=user, chart_period="last7"
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
"last30": list(
|
"last30": list(
|
||||||
live_youtube_channel_charts(
|
live_youtube_channel_charts(user=user, chart_period="last30")
|
||||||
user=user, chart_period="last30"
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
"year": list(
|
"year": list(
|
||||||
live_youtube_channel_charts(user=user, chart_period="year")
|
live_youtube_channel_charts(user=user, chart_period="year")
|
||||||
),
|
),
|
||||||
"all": list(
|
"all": list(live_youtube_channel_charts(user=user, chart_period="all")),
|
||||||
live_youtube_channel_charts(user=user, chart_period="all")
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return context_data
|
return context_data
|
||||||
@ -955,9 +909,7 @@ class ChartRecordView(TemplateView):
|
|||||||
params["day"] = day
|
params["day"] = day
|
||||||
month_str = calendar.month_name[month]
|
month_str = calendar.month_name[month]
|
||||||
name = f"Chart for {month_str} {day}, {year}"
|
name = f"Chart for {month_str} {day}, {year}"
|
||||||
in_progress = (
|
in_progress = now.month == month and now.year == year and now.day == day
|
||||||
now.month == month and now.year == year and now.day == day
|
|
||||||
)
|
|
||||||
|
|
||||||
media_filter = self.get_media_filter("Track")
|
media_filter = self.get_media_filter("Track")
|
||||||
track_charts = ChartRecord.objects.filter(
|
track_charts = ChartRecord.objects.filter(
|
||||||
@ -969,17 +921,13 @@ class ChartRecordView(TemplateView):
|
|||||||
).order_by("rank")
|
).order_by("rank")
|
||||||
|
|
||||||
if track_charts.count() == 0 and not in_progress:
|
if track_charts.count() == 0 and not in_progress:
|
||||||
ChartRecord.build(
|
ChartRecord.build(user=self.request.user, model_str="Track", **params)
|
||||||
user=self.request.user, model_str="Track", **params
|
|
||||||
)
|
|
||||||
media_filter = self.get_media_filter("Track")
|
media_filter = self.get_media_filter("Track")
|
||||||
track_charts = ChartRecord.objects.filter(
|
track_charts = ChartRecord.objects.filter(
|
||||||
media_filter, user=self.request.user, **params
|
media_filter, user=self.request.user, **params
|
||||||
).order_by("rank")
|
).order_by("rank")
|
||||||
if artist_charts.count() == 0 and not in_progress:
|
if artist_charts.count() == 0 and not in_progress:
|
||||||
ChartRecord.build(
|
ChartRecord.build(user=self.request.user, model_str="Artist", **params)
|
||||||
user=self.request.user, model_str="Artist", **params
|
|
||||||
)
|
|
||||||
media_filter = self.get_media_filter("Artist")
|
media_filter = self.get_media_filter("Artist")
|
||||||
artist_charts = ChartRecord.objects.filter(
|
artist_charts = ChartRecord.objects.filter(
|
||||||
media_filter, user=self.request.user, **params
|
media_filter, user=self.request.user, **params
|
||||||
@ -1000,38 +948,24 @@ class ScrobbleStatusView(LoginRequiredMixin, TemplateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
user_scrobble_qs = Scrobble.objects.filter().order_by("-timestamp")
|
user_scrobble_qs = Scrobble.objects.filter().order_by("-timestamp")
|
||||||
progress_plays = user_scrobble_qs.filter(
|
progress_plays = user_scrobble_qs.filter(in_progress=True, is_paused=False)
|
||||||
in_progress=True, is_paused=False
|
|
||||||
)
|
|
||||||
|
|
||||||
data["listening"] = progress_plays.filter(
|
data["listening"] = progress_plays.filter(
|
||||||
Q(track__isnull=False) | Q(podcast_episode__isnull=False)
|
Q(track__isnull=False) | Q(podcast_episode__isnull=False)
|
||||||
).first()
|
).first()
|
||||||
data["watching"] = progress_plays.filter(video__isnull=False).first()
|
data["watching"] = progress_plays.filter(video__isnull=False).first()
|
||||||
data["going"] = progress_plays.filter(
|
data["going"] = progress_plays.filter(geo_location__isnull=False).first()
|
||||||
geo_location__isnull=False
|
data["playing"] = progress_plays.filter(board_game__isnull=False).first()
|
||||||
).first()
|
data["sporting"] = progress_plays.filter(sport_event__isnull=False).first()
|
||||||
data["playing"] = progress_plays.filter(
|
data["browsing"] = progress_plays.filter(web_page__isnull=False).first()
|
||||||
board_game__isnull=False
|
data["participating"] = progress_plays.filter(life_event__isnull=False).first()
|
||||||
).first()
|
|
||||||
data["sporting"] = progress_plays.filter(
|
|
||||||
sport_event__isnull=False
|
|
||||||
).first()
|
|
||||||
data["browsing"] = progress_plays.filter(
|
|
||||||
web_page__isnull=False
|
|
||||||
).first()
|
|
||||||
data["participating"] = progress_plays.filter(
|
|
||||||
life_event__isnull=False
|
|
||||||
).first()
|
|
||||||
data["working"] = progress_plays.filter(task__isnull=False).first()
|
data["working"] = progress_plays.filter(task__isnull=False).first()
|
||||||
|
|
||||||
long_plays = user_scrobble_qs.filter(
|
long_plays = user_scrobble_qs.filter(
|
||||||
long_play_complete=False, played_to_completion=True
|
long_play_complete=False, played_to_completion=True
|
||||||
)
|
)
|
||||||
data["reading"] = long_plays.filter(book__isnull=False).first()
|
data["reading"] = long_plays.filter(book__isnull=False).first()
|
||||||
data["sessioning"] = long_plays.filter(
|
data["sessioning"] = long_plays.filter(video_game__isnull=False).first()
|
||||||
video_game__isnull=False
|
|
||||||
).first()
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -1066,9 +1000,7 @@ class ScrobbleDetailView(DetailView):
|
|||||||
data[field_name] = original_value
|
data[field_name] = original_value
|
||||||
|
|
||||||
if data.get("with_people_ids", False):
|
if data.get("with_people_ids", False):
|
||||||
data["with_people_ids"] = [
|
data["with_people_ids"] = [p.id for p in data["with_people_ids"]]
|
||||||
p.id for p in data["with_people_ids"]
|
|
||||||
]
|
|
||||||
|
|
||||||
if data.get("platform_id", False):
|
if data.get("platform_id", False):
|
||||||
data["platform_id"] = data["platform_id"].id
|
data["platform_id"] = data["platform_id"].id
|
||||||
@ -1085,3 +1017,168 @@ class ScrobbleDetailView(DetailView):
|
|||||||
if "log_form" not in context:
|
if "log_form" not in context:
|
||||||
context["log_form"] = self.get_form()
|
context["log_form"] = self.get_form()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddableTopArtistWidget(TemplateView):
|
||||||
|
template_name = "scrobbles/embeddable_top_artist.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
user_id = self.kwargs.get("user_id")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
context["error"] = "User not found"
|
||||||
|
return context
|
||||||
|
|
||||||
|
profile = user.profile
|
||||||
|
if not profile.enable_public_widgets:
|
||||||
|
raise Http404("Public widgets are not enabled for this user")
|
||||||
|
|
||||||
|
context["user"] = user
|
||||||
|
|
||||||
|
artist = {
|
||||||
|
"user": user,
|
||||||
|
"media_type": "Artist",
|
||||||
|
"limit": 10,
|
||||||
|
}
|
||||||
|
context["top_artists"] = list(live_charts(**artist, chart_period="last7"))
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddableTopBoardGamesMonthWidget(TemplateView):
|
||||||
|
template_name = "scrobbles/embeddable_top_board_games_month.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
from boardgames.models import BoardGame
|
||||||
|
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
user_id = self.kwargs.get("user_id")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
context["error"] = "User not found"
|
||||||
|
return context
|
||||||
|
|
||||||
|
profile = user.profile
|
||||||
|
if not profile.enable_public_widgets:
|
||||||
|
raise Http404("Public widgets are not enabled for this user")
|
||||||
|
|
||||||
|
context["user"] = user
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
if user.is_authenticated:
|
||||||
|
now = now_user_timezone(user.profile)
|
||||||
|
now = now.date()
|
||||||
|
start_day_of_month = now.replace(day=1)
|
||||||
|
|
||||||
|
top_games = (
|
||||||
|
BoardGame.objects.filter(
|
||||||
|
scrobble__user=user,
|
||||||
|
scrobble__timestamp__gte=start_day_of_month,
|
||||||
|
scrobble__played_to_completion=True,
|
||||||
|
)
|
||||||
|
.annotate(num_plays=Count("scrobble", distinct=True))
|
||||||
|
.order_by("-num_plays")[:10]
|
||||||
|
)
|
||||||
|
|
||||||
|
context["top_board_games"] = list(top_games)
|
||||||
|
context["month"] = now.strftime("%B %Y")
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddableTopBoardGamesWeekWidget(TemplateView):
|
||||||
|
template_name = "scrobbles/embeddable_top_board_games_week.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
from boardgames.models import BoardGame
|
||||||
|
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
user_id = self.kwargs.get("user_id")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
context["error"] = "User not found"
|
||||||
|
return context
|
||||||
|
|
||||||
|
profile = user.profile
|
||||||
|
if not profile.enable_public_widgets:
|
||||||
|
raise Http404("Public widgets are not enabled for this user")
|
||||||
|
|
||||||
|
context["user"] = user
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
if user.is_authenticated:
|
||||||
|
now = now_user_timezone(user.profile)
|
||||||
|
now = now.date()
|
||||||
|
start_day_of_week = now - timedelta(days=now.isoweekday() % 7)
|
||||||
|
|
||||||
|
top_games = (
|
||||||
|
BoardGame.objects.filter(
|
||||||
|
scrobble__user=user,
|
||||||
|
scrobble__timestamp__gte=start_day_of_week,
|
||||||
|
scrobble__played_to_completion=True,
|
||||||
|
)
|
||||||
|
.annotate(num_plays=Count("scrobble", distinct=True))
|
||||||
|
.order_by("-num_plays")[:10]
|
||||||
|
)
|
||||||
|
|
||||||
|
context["top_board_games"] = list(top_games)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddableTopBoardGamesYearWidget(TemplateView):
|
||||||
|
template_name = "scrobbles/embeddable_top_board_games_year.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.utils import timezone
|
||||||
|
from boardgames.models import BoardGame
|
||||||
|
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
user_id = self.kwargs.get("user_id")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
context["error"] = "User not found"
|
||||||
|
return context
|
||||||
|
|
||||||
|
profile = user.profile
|
||||||
|
if not profile.enable_public_widgets:
|
||||||
|
raise Http404("Public widgets are not enabled for this user")
|
||||||
|
|
||||||
|
context["user"] = user
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
if user.is_authenticated:
|
||||||
|
now = now_user_timezone(user.profile)
|
||||||
|
now = now.date()
|
||||||
|
start_day_of_year = now.replace(month=1, day=1)
|
||||||
|
|
||||||
|
top_games = (
|
||||||
|
BoardGame.objects.filter(
|
||||||
|
scrobble__user=user,
|
||||||
|
scrobble__timestamp__gte=start_day_of_year,
|
||||||
|
scrobble__played_to_completion=True,
|
||||||
|
)
|
||||||
|
.annotate(num_plays=Count("scrobble", distinct=True))
|
||||||
|
.order_by("-num_plays")[:10]
|
||||||
|
)
|
||||||
|
|
||||||
|
context["top_board_games"] = list(top_games)
|
||||||
|
context["year"] = now.strftime("%Y")
|
||||||
|
|
||||||
|
return context
|
||||||
|
|||||||
87
vrobbler/templates/scrobbles/embeddable_top_artist.html
Normal file
87
vrobbler/templates/scrobbles/embeddable_top_artist.html
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{% load static %}
|
||||||
|
<style>
|
||||||
|
.vrobbler-widget {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
max-width: 400px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.vrobbler-widget h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.vrobbler-artist-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.vrobbler-artist-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.vrobbler-artist-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.vrobbler-rank {
|
||||||
|
width: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.vrobbler-artist-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.vrobbler-artist-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.vrobbler-scrobble-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.vrobbler-artist-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.vrobbler-error {
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
.vrobbler-no-data {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="vrobbler-widget">
|
||||||
|
<h3>Last 7 Days Top Artists{% if user %} - {{ user.username }}{% endif %}</h3>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="vrobbler-error">{{ error }}</p>
|
||||||
|
{% elif top_artists %}
|
||||||
|
<ul class="vrobbler-artist-list">
|
||||||
|
{% for artist in top_artists %}
|
||||||
|
<li class="vrobbler-artist-item">
|
||||||
|
<span class="vrobbler-rank">{{ forloop.counter }}</span>
|
||||||
|
{% if artist.thumbnail %}
|
||||||
|
<img class="vrobbler-artist-thumb" src="{{ artist.thumbnail_small.url }}" alt="{{ artist.name }}">
|
||||||
|
{% else %}
|
||||||
|
<img class="vrobbler-artist-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ artist.name }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="vrobbler-artist-info">
|
||||||
|
<div class="vrobbler-artist-name">{{ artist.name }}</div>
|
||||||
|
<div class="vrobbler-scrobble-count">{{ artist.num_scrobbles }} scrobbles</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="vrobbler-no-data">No scrobbles in the last 7 days</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
{% load static %}
|
||||||
|
<style>
|
||||||
|
.vrobbler-widget {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
max-width: 400px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.vrobbler-widget h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.vrobbler-rank {
|
||||||
|
width: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.vrobbler-play-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.vrobbler-error {
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
.vrobbler-no-data {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="vrobbler-widget">
|
||||||
|
<h3>Top Board Games - {{ month }}{% if user %} - {{ user.username }}{% endif %}</h3>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="vrobbler-error">{{ error }}</p>
|
||||||
|
{% elif top_board_games %}
|
||||||
|
<ul class="vrobbler-board-game-list">
|
||||||
|
{% for game in top_board_games %}
|
||||||
|
<li class="vrobbler-board-game-item">
|
||||||
|
<span class="vrobbler-rank">{{ forloop.counter }}</span>
|
||||||
|
{% if game.cover %}
|
||||||
|
<img class="vrobbler-board-game-thumb" src="{{ game.cover_small.url }}" alt="{{ game.title }}">
|
||||||
|
{% else %}
|
||||||
|
<img class="vrobbler-board-game-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ game.title }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="vrobbler-board-game-info">
|
||||||
|
<div class="vrobbler-board-game-title">{{ game.title }}</div>
|
||||||
|
<div class="vrobbler-play-count">{{ game.num_plays }} plays</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="vrobbler-no-data">No board games played this month</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
{% load static %}
|
||||||
|
<style>
|
||||||
|
.vrobbler-widget {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
max-width: 400px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.vrobbler-widget h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.vrobbler-rank {
|
||||||
|
width: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.vrobbler-play-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.vrobbler-error {
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
.vrobbler-no-data {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="vrobbler-widget">
|
||||||
|
<h3>Top Board Games - This Week{% if user %} - {{ user.username }}{% endif %}</h3>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="vrobbler-error">{{ error }}</p>
|
||||||
|
{% elif top_board_games %}
|
||||||
|
<ul class="vrobbler-board-game-list">
|
||||||
|
{% for game in top_board_games %}
|
||||||
|
<li class="vrobbler-board-game-item">
|
||||||
|
<span class="vrobbler-rank">{{ forloop.counter }}</span>
|
||||||
|
{% if game.cover %}
|
||||||
|
<img class="vrobbler-board-game-thumb" src="{{ game.cover_small.url }}" alt="{{ game.title }}">
|
||||||
|
{% else %}
|
||||||
|
<img class="vrobbler-board-game-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ game.title }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="vrobbler-board-game-info">
|
||||||
|
<div class="vrobbler-board-game-title">{{ game.title }}</div>
|
||||||
|
<div class="vrobbler-play-count">{{ game.num_plays }} plays</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="vrobbler-no-data">No board games played this week</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
{% load static %}
|
||||||
|
<style>
|
||||||
|
.vrobbler-widget {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
max-width: 400px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.vrobbler-widget h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.vrobbler-rank {
|
||||||
|
width: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.vrobbler-play-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.vrobbler-board-game-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.vrobbler-error {
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
.vrobbler-no-data {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="vrobbler-widget">
|
||||||
|
<h3>Top Board Games - {{ year }}{% if user %} - {{ user.username }}{% endif %}</h3>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="vrobbler-error">{{ error }}</p>
|
||||||
|
{% elif top_board_games %}
|
||||||
|
<ul class="vrobbler-board-game-list">
|
||||||
|
{% for game in top_board_games %}
|
||||||
|
<li class="vrobbler-board-game-item">
|
||||||
|
<span class="vrobbler-rank">{{ forloop.counter }}</span>
|
||||||
|
{% if game.cover %}
|
||||||
|
<img class="vrobbler-board-game-thumb" src="{{ game.cover_small.url }}" alt="{{ game.title }}">
|
||||||
|
{% else %}
|
||||||
|
<img class="vrobbler-board-game-thumb" src="{% static 'images/not-found.jpg' %}" alt="{{ game.title }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="vrobbler-board-game-info">
|
||||||
|
<div class="vrobbler-board-game-title">{{ game.title }}</div>
|
||||||
|
<div class="vrobbler-play-count">{{ game.num_plays }} plays</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="vrobbler-no-data">No board games played this year</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user