Compare commits

..

No commits in common. "84dafe922394e6e0a057a0ae10af7faa7cb52aea" and "84c92fe654e8cdd06d58e7f0fbe5702863a38a3c" have entirely different histories.

11 changed files with 78 additions and 182 deletions

View File

@ -1,8 +0,0 @@
## Unreleased
* Hide navigation bar items if there are no games/purchases/sessions
* Set default version to "git-main" to indicate development environment
* Add homepage, link to it from the logo
* Make it possible to add a new platform
* Save calculated duration to database if both timestamps are set
* Improve session listing
* Set version in the footer to fixed, fix main container height

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from .models import Session, Purchase, Game, Platform from .models import Session, Purchase, Game
class SessionForm(forms.ModelForm): class SessionForm(forms.ModelForm):
@ -24,9 +24,3 @@ class GameForm(forms.ModelForm):
class Meta: class Meta:
model = Game model = Game
fields = ["name", "wikidata"] fields = ["name", "wikidata"]
class PlatformForm(forms.ModelForm):
class Meta:
model = Platform
fields = ["name", "group"]

View File

@ -38,36 +38,17 @@ class Session(models.Model):
def __str__(self): def __str__(self):
mark = ", manual" if self.duration_manual != None else "" mark = ", manual" if self.duration_manual != None else ""
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_any()}{mark})" return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.total_duration()}{mark})"
def duration_seconds(self): def calculated_duration(self):
if self.timestamp_end == None or self.timestamp_start == None: if self.timestamp_end == None or self.timestamp_start == None:
if self.duration_manual == None: return 0
return 0
else:
value = self.duration_manual
else: else:
value = self.timestamp_end - self.timestamp_start return self.timestamp_end - self.timestamp_start
return value.total_seconds()
def duration_formatted(self): def total_duration(self):
seconds = self.duration_seconds()
if seconds == 0:
return seconds
hours, remainder = divmod(seconds, 3600)
minutes = remainder % 60
hour_string = f"{int(hours)}h" if hours != 0 else ""
minute_string = f"{int(minutes)}m" if minutes != 0 else ""
return f"{hour_string}{minute_string}"
def duration_any(self):
return ( return (
self.duration_formatted() self.calculated_duration()
if self.duration_manual == None if self.duration_manual == None
else self.duration_manual else self.duration_manual + self.calculated_duration()
) )
def save(self, *args, **kwargs):
if self.timestamp_start != None and self.timestamp_end != None:
self.duration_calculated = self.timestamp_end - self.timestamp_start
super(Session, self).save(*args, **kwargs)

View File

@ -717,16 +717,24 @@ select {
position: static; position: static;
} }
.fixed { .absolute {
position: fixed; position: absolute;
} }
.left-2 { .left-0 {
left: 0.5rem; left: 0px;
} }
.bottom-2 { .bottom-0 {
bottom: 0.5rem; bottom: 0px;
}
.left-1 {
left: 0.25rem;
}
.bottom-1 {
bottom: 0.25rem;
} }
.mx-auto { .mx-auto {
@ -754,8 +762,8 @@ select {
display: grid; display: grid;
} }
.min-h-screen { .h-screen {
min-height: 100vh; height: 100vh;
} }
.w-full { .w-full {
@ -810,35 +818,16 @@ select {
border-radius: 0.75rem; border-radius: 0.75rem;
} }
.border {
border-width: 1px;
}
.border-gray-200 { .border-gray-200 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity)); border-color: rgb(229 231 235 / var(--tw-border-opacity));
} }
.border-red-800 {
--tw-border-opacity: 1;
border-color: rgb(153 27 27 / var(--tw-border-opacity));
}
.border-red-900 {
--tw-border-opacity: 1;
border-color: rgb(127 29 29 / var(--tw-border-opacity));
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
} }
.bg-red-700 {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.p-4 { .p-4 {
padding: 1rem; padding: 1rem;
} }
@ -847,10 +836,6 @@ select {
padding: 0.5rem; padding: 0.5rem;
} }
.p-1 {
padding: 0.25rem;
}
.py-2 { .py-2 {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
@ -878,11 +863,6 @@ select {
line-height: 1.75rem; line-height: 1.75rem;
} }
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-lg { .text-lg {
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.75rem; line-height: 1.75rem;
@ -893,6 +873,11 @@ select {
line-height: 1.25rem; line-height: 1.25rem;
} }
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-semibold { .font-semibold {
font-weight: 600; font-weight: 600;
} }
@ -902,6 +887,31 @@ select {
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.text-slate-800 {
--tw-text-opacity: 1;
color: rgb(30 41 59 / var(--tw-text-opacity));
}
.text-slate-700 {
--tw-text-opacity: 1;
color: rgb(51 65 85 / var(--tw-text-opacity));
}
.text-slate-600 {
--tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity));
}
.text-slate-500 {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity));
}
.text-slate-400 {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
}
.text-slate-300 { .text-slate-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity)); color: rgb(203 213 225 / var(--tw-text-opacity));
@ -934,35 +944,6 @@ form input[type=submit] {
padding: 0.5rem; padding: 0.5rem;
} }
.hover\:border-dotted:hover {
border-style: dotted;
}
.hover\:border-white:hover {
--tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity));
}
.hover\:bg-red-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.hover\:bg-red-500:hover {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.hover\:bg-yellow-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(161 98 7 / var(--tw-bg-opacity));
}
.hover\:bg-orange-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
}
.hover\:underline:hover { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
@ -987,16 +968,16 @@ form input[type=submit] {
background-color: rgb(51 65 85 / var(--tw-bg-opacity)); background-color: rgb(51 65 85 / var(--tw-bg-opacity));
} }
.dark .dark\:bg-slate-400 {
--tw-bg-opacity: 1;
background-color: rgb(148 163 184 / var(--tw-bg-opacity));
}
.dark .dark\:text-white { .dark .dark\:text-white {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.dark .dark\:text-slate-600 {
--tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity));
}
.dark .dark\:text-slate-400 { .dark .dark\:text-slate-400 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity)); color: rgb(148 163 184 / var(--tw-text-opacity));
@ -1007,6 +988,11 @@ form input[type=submit] {
color: rgb(203 213 225 / var(--tw-text-opacity)); color: rgb(203 213 225 / var(--tw-text-opacity));
} }
.dark .dark\:text-slate-600 {
--tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity));
}
@media (min-width: 768px) { @media (min-width: 768px) {
.md\:block { .md\:block {
display: block; display: block;

View File

@ -12,10 +12,10 @@
</head> </head>
<body class="dark"> <body class="dark">
<div class="dark:bg-gray-800 min-h-screen"> <div class="dark:bg-gray-800 h-screen">
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded"> <nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
<div class="container flex flex-wrap items-center justify-between mx-auto"> <div class="container flex flex-wrap items-center justify-between mx-auto">
<a href="{% url 'index' %}" class="flex items-center"> <a href="#" class="flex items-center">
<span class="text-4xl"></span> <span class="text-4xl"></span>
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span> <span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
</a> </a>
@ -23,16 +23,9 @@
<ul <ul
class="flex flex-col md:flex-row p-4 mt-4 dark:text-white"> class="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_game' %}">New Game</a></li> <li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_game' %}">New Game</a></li>
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_platform' %}">New Platform</a></li>
{% if game_available and platform_available %}
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_purchase' %}">New Purchase</a></li> <li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_purchase' %}">New Purchase</a></li>
{% endif %}
{% if purchase_available %}
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_session' %}">New Session</a></li> <li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_session' %}">New Session</a></li>
{% endif %}
{% if session_count > 0 %}
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'list_sessions' %}">All Sessions</a></li> <li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'list_sessions' %}">All Sessions</a></li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -40,7 +33,7 @@
{% block content %}No content here.{% endblock %} {% block content %}No content here.{% endblock %}
</div> </div>
{% load version %} {% load version %}
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span> <span id="version-info" class="absolute left-1 bottom-1 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
</body> </body>
</html> </html>

View File

@ -1,13 +0,0 @@
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock title %}
{% block content %}
<div class="text-slate-300 mx-auto max-w-screen-lg text-center">
{% if session_count > 0 %}
You have played a total of {{ session_count }} sessions.
{% else %}
Start by clicking the links at the top. To track playtime, you need to have at least 1 owned game.
{% endif %}
</div>
{% endblock content %}

View File

@ -17,17 +17,8 @@
{% for data in dataset %} {% for data in dataset %}
<div class=""><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div> <div class=""><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div>
<div class="dark:text-slate-400">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div> <div class="dark:text-slate-400">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
<div class="dark:text-slate-400"> <div class="dark:text-slate-400">{{ data.timestamp_end | date:"d/m/Y H:i" }}</div>
{% if data.unfinished %} <div class="dark:text-slate-400">{{ data.time_delta }}</div>
Not finished yet. <button class="bg-red-700 hover:bg-orange-700 border border-red-900 hover:border-dotted hover:border-white rounded p-1 text-white text-sm">Finish now?</button>
{% elif data.duration_manual %}
MANUAL
{% else %}
{{ data.timestamp_end | date:"d/m/Y H:i" }}
{% endif %}
</div>
{% load time %}
<div class="dark:text-slate-400">{{ data.duration_formatted }}{% if data.duration_manual %} (M){% endif %}</div>
{% endfor %} {% endfor %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -15,4 +15,4 @@ def version_date():
@register.simple_tag @register.simple_tag
def version(): def version():
return os.environ.get("VERSION_NUMBER", "git-main") return os.environ.get("VERSION_NUMBER", "UNKNOWN VERSION")

View File

@ -3,9 +3,7 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"),
path("add-game/", views.add_game, name="add_game"), path("add-game/", views.add_game, name="add_game"),
path("add-platform/", views.add_platform, name="add_platform"),
path("add-session/", views.add_session, name="add_session"), path("add-session/", views.add_session, name="add_session"),
path("add-purchase/", views.add_purchase, name="add_purchase"), path("add-purchase/", views.add_purchase, name="add_purchase"),
path("list-sessions/", views.list_sessions, name="list_sessions"), path("list-sessions/", views.list_sessions, name="list_sessions"),

View File

@ -1,19 +1,10 @@
from django.shortcuts import render from django.shortcuts import render
from .models import Game, Platform, Purchase, Session from .models import Game, Platform, Purchase, Session
from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm from .forms import SessionForm, PurchaseForm, GameForm
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo from django.db.models import ExpressionWrapper, F, DurationField
from django.conf import settings import logging
def model_counts(request):
return {
"game_available": Game.objects.count() != 0,
"platform_available": Platform.objects.count() != 0,
"purchase_available": Purchase.objects.count() != 0,
"session_count": Session.objects.count(),
}
def add_session(request): def add_session(request):
@ -37,11 +28,11 @@ def list_sessions(request, purchase_id=None):
else: else:
dataset = Session.objects.all() dataset = Session.objects.all()
for session in dataset: dataset = dataset.annotate(
if session.timestamp_end == None and session.duration_manual == None: time_delta=ExpressionWrapper(
session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) F("timestamp_end") - F("timestamp_start"), output_field=DurationField()
session.unfinished = True )
)
context["dataset"] = dataset context["dataset"] = dataset
return render(request, "list_sessions.html", context) return render(request, "list_sessions.html", context)
@ -69,19 +60,3 @@ def add_game(request):
context["form"] = form context["form"] = form
context["title"] = "Add New Game" context["title"] = "Add New Game"
return render(request, "add.html", context) return render(request, "add.html", context)
def add_platform(request):
context = {}
form = PlatformForm(request.POST or None)
if form.is_valid():
form.save()
context["form"] = form
context["title"] = "Add New Platform"
return render(request, "add.html", context)
def index(request):
context = {}
return render(request, "index.html", context)

View File

@ -65,7 +65,6 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"tracker.views.model_counts",
], ],
}, },
}, },