Compare commits

..

8 Commits

Author SHA1 Message Date
Lukáš Kucharczyk 84dafe9223
Update generated CSS
continuous-integration/drone/push Build is passing Details
2023-01-04 17:29:19 +01:00
Lukáš Kucharczyk 59cf620ff3
Set version in the footer to fixed, fix main container height 2023-01-04 17:28:51 +01:00
Lukáš Kucharczyk 40810256aa
Improve session listing 2023-01-04 17:27:54 +01:00
Lukáš Kucharczyk b3842504af
Save calculated duration to database 2023-01-04 17:25:19 +01:00
Lukáš Kucharczyk bf61326c18
Make it possible to add a new platform 2023-01-04 17:24:54 +01:00
Lukáš Kucharczyk 4c642d97cb
Add homepage, link to it from the logo 2023-01-04 17:22:36 +01:00
Lukáš Kucharczyk d225856174
Set default version to "git-main" 2023-01-04 17:20:46 +01:00
Lukáš Kucharczyk 5c50e059e6
Hide navigation bar items
If there are no games/purchases/sessions,
hide the related navbar items
2023-01-04 17:19:40 +01:00
11 changed files with 182 additions and 78 deletions

8
CHANGELOG.md Normal file
View File

@ -0,0 +1,8 @@
## 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 .models import Session, Purchase, Game
from .models import Session, Purchase, Game, Platform
class SessionForm(forms.ModelForm):
@ -24,3 +24,9 @@ class GameForm(forms.ModelForm):
class Meta:
model = Game
fields = ["name", "wikidata"]
class PlatformForm(forms.ModelForm):
class Meta:
model = Platform
fields = ["name", "group"]

View File

@ -38,17 +38,36 @@ class Session(models.Model):
def __str__(self):
mark = ", manual" if self.duration_manual != None else ""
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.total_duration()}{mark})"
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_any()}{mark})"
def calculated_duration(self):
def duration_seconds(self):
if self.timestamp_end == None or self.timestamp_start == None:
return 0
if self.duration_manual == None:
return 0
else:
value = self.duration_manual
else:
return self.timestamp_end - self.timestamp_start
value = self.timestamp_end - self.timestamp_start
return value.total_seconds()
def total_duration(self):
def duration_formatted(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 (
self.calculated_duration()
self.duration_formatted()
if self.duration_manual == None
else self.duration_manual + self.calculated_duration()
else self.duration_manual
)
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,24 +717,16 @@ select {
position: static;
}
.absolute {
position: absolute;
.fixed {
position: fixed;
}
.left-0 {
left: 0px;
.left-2 {
left: 0.5rem;
}
.bottom-0 {
bottom: 0px;
}
.left-1 {
left: 0.25rem;
}
.bottom-1 {
bottom: 0.25rem;
.bottom-2 {
bottom: 0.5rem;
}
.mx-auto {
@ -762,8 +754,8 @@ select {
display: grid;
}
.h-screen {
height: 100vh;
.min-h-screen {
min-height: 100vh;
}
.w-full {
@ -818,16 +810,35 @@ select {
border-radius: 0.75rem;
}
.border {
border-width: 1px;
}
.border-gray-200 {
--tw-border-opacity: 1;
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 {
--tw-bg-opacity: 1;
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 {
padding: 1rem;
}
@ -836,6 +847,10 @@ select {
padding: 0.5rem;
}
.p-1 {
padding: 0.25rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
@ -863,6 +878,11 @@ select {
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
@ -873,11 +893,6 @@ select {
line-height: 1.25rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-semibold {
font-weight: 600;
}
@ -887,31 +902,6 @@ select {
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 {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity));
@ -944,6 +934,35 @@ form input[type=submit] {
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 {
text-decoration-line: underline;
}
@ -968,16 +987,16 @@ form input[type=submit] {
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 {
--tw-text-opacity: 1;
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 {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
@ -988,11 +1007,6 @@ form input[type=submit] {
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) {
.md\:block {
display: block;

View File

@ -12,10 +12,10 @@
</head>
<body class="dark">
<div class="dark:bg-gray-800 h-screen">
<div class="dark:bg-gray-800 min-h-screen">
<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">
<a href="#" class="flex items-center">
<a href="{% url 'index' %}" class="flex items-center">
<span class="text-4xl"></span>
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
</a>
@ -23,9 +23,16 @@
<ul
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_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>
{% endif %}
{% if purchase_available %}
<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>
{% endif %}
</ul>
</div>
</div>
@ -33,7 +40,7 @@
{% block content %}No content here.{% endblock %}
</div>
{% load version %}
<span id="version-info" class="absolute left-1 bottom-1 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
</body>
</html>

View File

@ -0,0 +1,13 @@
{% 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,8 +17,17 @@
{% 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="dark:text-slate-400">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
<div class="dark:text-slate-400">{{ data.timestamp_end | date:"d/m/Y H:i" }}</div>
<div class="dark:text-slate-400">{{ data.time_delta }}</div>
<div class="dark:text-slate-400">
{% if data.unfinished %}
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 %}
</div>
{% endblock content %}

View File

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

View File

@ -3,7 +3,9 @@ from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
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-purchase/", views.add_purchase, name="add_purchase"),
path("list-sessions/", views.list_sessions, name="list_sessions"),

View File

@ -1,10 +1,19 @@
from django.shortcuts import render
from .models import Game, Platform, Purchase, Session
from .forms import SessionForm, PurchaseForm, GameForm
from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm
from datetime import datetime
from django.db.models import ExpressionWrapper, F, DurationField
import logging
from zoneinfo import ZoneInfo
from django.conf import settings
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):
@ -28,11 +37,11 @@ def list_sessions(request, purchase_id=None):
else:
dataset = Session.objects.all()
dataset = dataset.annotate(
time_delta=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"), output_field=DurationField()
)
)
for session in dataset:
if session.timestamp_end == None and session.duration_manual == None:
session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE))
session.unfinished = True
context["dataset"] = dataset
return render(request, "list_sessions.html", context)
@ -60,3 +69,19 @@ def add_game(request):
context["form"] = form
context["title"] = "Add New Game"
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,6 +65,7 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"tracker.views.model_counts",
],
},
},