Compare commits
	
		
			2 Commits
		
	
	
		
			9a332593f4
			...
			c814b4c2cb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c814b4c2cb | |||
| 11b9c602de | 
@ -18,6 +18,7 @@ steps:
 | 
			
		||||
    repo: registry.kucharczyk.xyz/timetracker
 | 
			
		||||
    tags:
 | 
			
		||||
      - latest
 | 
			
		||||
      - 1.1.0
 | 
			
		||||
  depends_on:
 | 
			
		||||
    - "test"
 | 
			
		||||
  when:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
## Unreleased
 | 
			
		||||
## 1.1.0 / 2023-10-09 00:01+02:00
 | 
			
		||||
 | 
			
		||||
### New
 | 
			
		||||
* Add game overview page (https://git.kucharczyk.xyz/lukas/timetracker/issues/8)
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ RUN npm install && \
 | 
			
		||||
 | 
			
		||||
FROM python:3.10.9-slim-bullseye
 | 
			
		||||
 | 
			
		||||
ENV VERSION_NUMBER 1.0.3
 | 
			
		||||
ENV VERSION_NUMBER 1.1.0
 | 
			
		||||
ENV PROD 1
 | 
			
		||||
ENV PYTHONUNBUFFERED=1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -36,9 +36,9 @@ def format_duration(
 | 
			
		||||
    minute_seconds = 60
 | 
			
		||||
    hour_seconds = 60 * minute_seconds
 | 
			
		||||
    day_seconds = 24 * hour_seconds
 | 
			
		||||
    duration = _safe_timedelta(duration)
 | 
			
		||||
    safe_duration = _safe_timedelta(duration)
 | 
			
		||||
    # we don't need float
 | 
			
		||||
    seconds_total = int(duration.total_seconds())
 | 
			
		||||
    seconds_total = int(safe_duration.total_seconds())
 | 
			
		||||
    # timestamps where end is before start
 | 
			
		||||
    if seconds_total < 0:
 | 
			
		||||
        seconds_total = 0
 | 
			
		||||
 | 
			
		||||
@ -73,11 +73,14 @@ class Platform(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SessionQuerySet(models.QuerySet):
 | 
			
		||||
    def total_duration(self):
 | 
			
		||||
    def total_duration_formatted(self):
 | 
			
		||||
        return format_duration(self.total_duration_unformatted())
 | 
			
		||||
 | 
			
		||||
    def total_duration_unformatted(self):
 | 
			
		||||
        result = self.aggregate(
 | 
			
		||||
            duration=Sum(F("duration_calculated") + F("duration_manual"))
 | 
			
		||||
        )
 | 
			
		||||
        return format_duration(result["duration"])
 | 
			
		||||
        return result["duration"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Session(models.Model):
 | 
			
		||||
@ -116,7 +119,7 @@ class Session(models.Model):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def duration_sum(self) -> str:
 | 
			
		||||
        return Session.objects.all().total_duration()
 | 
			
		||||
        return Session.objects.all().total_duration_formatted()
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if self.timestamp_start != None and self.timestamp_end != None:
 | 
			
		||||
 | 
			
		||||
@ -771,24 +771,14 @@ select {
 | 
			
		||||
  top: 0.75rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx-auto {
 | 
			
		||||
  margin-left: auto;
 | 
			
		||||
  margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-4 {
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx-2 {
 | 
			
		||||
  margin-left: 0.5rem;
 | 
			
		||||
  margin-right: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-1 {
 | 
			
		||||
  margin-top: 0.25rem;
 | 
			
		||||
  margin-bottom: 0.25rem;
 | 
			
		||||
.mx-auto {
 | 
			
		||||
  margin-left: auto;
 | 
			
		||||
  margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-2 {
 | 
			
		||||
@ -796,10 +786,23 @@ select {
 | 
			
		||||
  margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-4 {
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mb-1 {
 | 
			
		||||
  margin-bottom: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mb-4 {
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ml-2 {
 | 
			
		||||
  margin-left: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mr-4 {
 | 
			
		||||
  margin-right: 1rem;
 | 
			
		||||
}
 | 
			
		||||
@ -808,30 +811,6 @@ select {
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mt-1 {
 | 
			
		||||
  margin-top: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mt-2 {
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mb-2 {
 | 
			
		||||
  margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mb-1 {
 | 
			
		||||
  margin-bottom: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ml-1 {
 | 
			
		||||
  margin-left: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ml-2 {
 | 
			
		||||
  margin-left: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block {
 | 
			
		||||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
@ -848,10 +827,6 @@ select {
 | 
			
		||||
  display: table;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-item {
 | 
			
		||||
  display: list-item;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hidden {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
@ -872,30 +847,18 @@ select {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.w-5 {
 | 
			
		||||
  width: 1.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-screen-lg {
 | 
			
		||||
  max-width: 1024px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-xs {
 | 
			
		||||
  max-width: 20rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-lg {
 | 
			
		||||
  max-width: 32rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-3xl {
 | 
			
		||||
  max-width: 48rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-sm {
 | 
			
		||||
  max-width: 24rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-xs {
 | 
			
		||||
  max-width: 20rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
  to {
 | 
			
		||||
    transform: rotate(360deg);
 | 
			
		||||
@ -906,10 +869,6 @@ select {
 | 
			
		||||
  animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list-disc {
 | 
			
		||||
  list-style-type: disc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex-col {
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
@ -953,16 +912,6 @@ select {
 | 
			
		||||
  border-color: rgb(229 231 235 / var(--tw-border-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.border-red-300 {
 | 
			
		||||
  --tw-border-opacity: 1;
 | 
			
		||||
  border-color: rgb(252 165 165 / var(--tw-border-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.border-red-500 {
 | 
			
		||||
  --tw-border-opacity: 1;
 | 
			
		||||
  border-color: rgb(239 68 68 / var(--tw-border-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.border-slate-500 {
 | 
			
		||||
  --tw-border-opacity: 1;
 | 
			
		||||
  border-color: rgb(100 116 139 / var(--tw-border-opacity));
 | 
			
		||||
@ -978,11 +927,6 @@ select {
 | 
			
		||||
  background-color: rgb(255 255 255 / var(--tw-bg-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bg-slate-400 {
 | 
			
		||||
  --tw-bg-opacity: 1;
 | 
			
		||||
  background-color: rgb(148 163 184 / var(--tw-bg-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.p-4 {
 | 
			
		||||
  padding: 1rem;
 | 
			
		||||
}
 | 
			
		||||
@ -1018,12 +962,9 @@ select {
 | 
			
		||||
  font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.font-serif {
 | 
			
		||||
  font-family: IBM Plex Serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.font-sans {
 | 
			
		||||
  font-family: IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
 | 
			
		||||
.text-3xl {
 | 
			
		||||
  font-size: 1.875rem;
 | 
			
		||||
  line-height: 2.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-4xl {
 | 
			
		||||
@ -1036,6 +977,11 @@ select {
 | 
			
		||||
  line-height: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-lg {
 | 
			
		||||
  font-size: 1.125rem;
 | 
			
		||||
  line-height: 1.75rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-xl {
 | 
			
		||||
  font-size: 1.25rem;
 | 
			
		||||
  line-height: 1.75rem;
 | 
			
		||||
@ -1046,62 +992,10 @@ select {
 | 
			
		||||
  line-height: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-2xl {
 | 
			
		||||
  font-size: 1.5rem;
 | 
			
		||||
  line-height: 2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-9xl {
 | 
			
		||||
  font-size: 8rem;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-8xl {
 | 
			
		||||
  font-size: 6rem;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-7xl {
 | 
			
		||||
  font-size: 4.5rem;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-6xl {
 | 
			
		||||
  font-size: 3.75rem;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-5xl {
 | 
			
		||||
  font-size: 3rem;
 | 
			
		||||
  line-height: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-lg {
 | 
			
		||||
  font-size: 1.125rem;
 | 
			
		||||
  line-height: 1.75rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-3xl {
 | 
			
		||||
  font-size: 1.875rem;
 | 
			
		||||
  line-height: 2.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.font-semibold {
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.font-bold {
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.capitalize {
 | 
			
		||||
  text-transform: capitalize;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.italic {
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-slate-300 {
 | 
			
		||||
  --tw-text-opacity: 1;
 | 
			
		||||
  color: rgb(203 213 225 / var(--tw-text-opacity));
 | 
			
		||||
@ -1117,23 +1011,10 @@ select {
 | 
			
		||||
  color: rgb(253 224 71 / var(--tw-text-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-gray-600 {
 | 
			
		||||
  --tw-text-opacity: 1;
 | 
			
		||||
  color: rgb(75 85 99 / var(--tw-text-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.underline {
 | 
			
		||||
  text-decoration-line: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.decoration-red-500 {
 | 
			
		||||
  text-decoration-color: #ef4444;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.decoration-slate-400 {
 | 
			
		||||
  text-decoration-color: #94a3b8;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.decoration-slate-500 {
 | 
			
		||||
  text-decoration-color: #64748b;
 | 
			
		||||
}
 | 
			
		||||
@ -1403,11 +1284,17 @@ th label {
 | 
			
		||||
  color: rgb(255 255 255 / var(--tw-text-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 640px) {
 | 
			
		||||
  .sm\:ml-2 {
 | 
			
		||||
    margin-left: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
:is(.dark .dark\:text-slate-400) {
 | 
			
		||||
  --tw-text-opacity: 1;
 | 
			
		||||
  color: rgb(148 163 184 / var(--tw-text-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:is(.dark .dark\:text-slate-500) {
 | 
			
		||||
  --tw-text-opacity: 1;
 | 
			
		||||
  color: rgb(100 116 139 / var(--tw-text-opacity));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 640px) {
 | 
			
		||||
  .sm\:inline {
 | 
			
		||||
    display: inline;
 | 
			
		||||
  }
 | 
			
		||||
@ -1420,10 +1307,6 @@ th label {
 | 
			
		||||
    max-width: 28rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sm\:max-w-lg {
 | 
			
		||||
    max-width: 32rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sm\:max-w-xl {
 | 
			
		||||
    max-width: 36rem;
 | 
			
		||||
  }
 | 
			
		||||
@ -1475,11 +1358,11 @@ th label {
 | 
			
		||||
    display: table-cell;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .lg\:max-w-lg {
 | 
			
		||||
    max-width: 32rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .lg\:max-w-3xl {
 | 
			
		||||
    max-width: 48rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .lg\:max-w-lg {
 | 
			
		||||
    max-width: 32rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
{% 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 for a total of {{ total_duration }}.
 | 
			
		||||
            You have played a total of {{ session_count }} sessions for a total of {{ total_duration_formatted }}.
 | 
			
		||||
        {% elif not game_available or not platform_available %}
 | 
			
		||||
            There are no games in the database. Start by clicking "New Game" and "New Platform".
 | 
			
		||||
        {% elif not purchase_available %}
 | 
			
		||||
@ -14,4 +14,4 @@
 | 
			
		||||
            You haven't played any games yet. Click "New Session" to add one now.
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock content %}
 | 
			
		||||
{% endblock content %}
 | 
			
		||||
 | 
			
		||||
@ -6,10 +6,15 @@
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
 | 
			
		||||
        <h1 class="text-4xl">{{ game.name }} (#{{ game.pk }})</h1>
 | 
			
		||||
        <h2 class="text-lg my-2 ml-2">{{ total_playtime }} ({{ first_session.timestamp_start | date:"M Y"}} — {{ last_session.timestamp_start | date:"M Y"}}) </h2>
 | 
			
		||||
        <h1 class="text-4xl">{{ game.name }} <span class="dark:text-slate-500">(#{{ game.pk }})</span></h1>
 | 
			
		||||
        <h2 class="text-lg my-2 ml-2">
 | 
			
		||||
            {{ total_hours }} <span class="dark:text-slate-500">total</span>
 | 
			
		||||
            {{ session_average }} <span class="dark:text-slate-500">avg</span> 
 | 
			
		||||
            ({{ first_session.timestamp_start | date:"M Y"}}
 | 
			
		||||
             — 
 | 
			
		||||
            {{ last_session.timestamp_start | date:"M Y"}}) </h2>
 | 
			
		||||
        <hr class="border-slate-500">
 | 
			
		||||
        <h1 class="text-3xl mt-4 mb-1">Editions</h1>
 | 
			
		||||
        <h1 class="text-3xl mt-4 mb-1">Editions <span class="dark:text-slate-500">({{ editions.count }})</span></h1>
 | 
			
		||||
        <ul>
 | 
			
		||||
            {% for edition in editions %}
 | 
			
		||||
            <li class="sm:pl-2">
 | 
			
		||||
@ -24,13 +29,13 @@
 | 
			
		||||
            </li>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </ul>
 | 
			
		||||
        <h1 class="text-3xl mt-4 mb-1">Purchases</h1>
 | 
			
		||||
        <h1 class="text-3xl mt-4 mb-1">Purchases <span class="dark:text-slate-500">({{ purchases.count }})</span></h1>
 | 
			
		||||
        <ul>
 | 
			
		||||
            {% for purchase in purchases %}
 | 
			
		||||
            <li class="sm:pl-2">{{ purchase.platform }} ({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})</li>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </ul>
 | 
			
		||||
        <h1 class="text-3xl mt-4 mb-1">Sessions</h1>
 | 
			
		||||
        <h1 class="text-3xl mt-4 mb-1">Sessions <span class="dark:text-slate-500">({{ sessions.count }})</span></h1>
 | 
			
		||||
        <ul>
 | 
			
		||||
            {% for session in sessions %}
 | 
			
		||||
            <li class="sm:pl-2">{{ session.timestamp_start | date:"d/m/Y" }} ({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }})</li>
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ from datetime import datetime, timedelta
 | 
			
		||||
from zoneinfo import ZoneInfo
 | 
			
		||||
 | 
			
		||||
from common.time import now as now_with_tz
 | 
			
		||||
from common.time import format_duration
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import redirect, render
 | 
			
		||||
 | 
			
		||||
@ -100,7 +101,12 @@ def view_game(request, game_id=None):
 | 
			
		||||
    context["sessions"] = Session.objects.filter(
 | 
			
		||||
        purchase__edition__game_id=game_id
 | 
			
		||||
    ).order_by("-timestamp_start")
 | 
			
		||||
    context["total_playtime"] = context["sessions"].total_duration()
 | 
			
		||||
    context["total_hours"] = int(
 | 
			
		||||
        format_duration(context["sessions"].total_duration_unformatted(), "%H")
 | 
			
		||||
    )
 | 
			
		||||
    context["session_average"] = round(
 | 
			
		||||
        (context["total_hours"]) / int(context["sessions"].count()), 1
 | 
			
		||||
    )
 | 
			
		||||
    # here first and last is flipped
 | 
			
		||||
    # because sessions are ordered from newest to oldest
 | 
			
		||||
    # so the most recent are on top
 | 
			
		||||
@ -197,7 +203,7 @@ def list_sessions(
 | 
			
		||||
            session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE))
 | 
			
		||||
            session.unfinished = True
 | 
			
		||||
 | 
			
		||||
    context["total_duration"] = dataset.total_duration()
 | 
			
		||||
    context["total_duration"] = dataset.total_duration_formatted()
 | 
			
		||||
    context["dataset"] = dataset
 | 
			
		||||
    # cannot use dataset[0] here because that might be only partial QuerySet
 | 
			
		||||
    context["last"] = Session.objects.all().order_by("timestamp_start").last()
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
[tool.poetry]
 | 
			
		||||
name = "timetracker"
 | 
			
		||||
version = "1.0.3"
 | 
			
		||||
version = "1.1.0"
 | 
			
		||||
description = "A simple time tracker."
 | 
			
		||||
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
 | 
			
		||||
license = "GPL"
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user