diff --git a/CHANGELOG.md b/CHANGELOG.md index 670ee85..b166331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Add all-time stats * Manage purchases * Automatically convert purchase prices +* Add emulated property to sessions ## Improved * mark refunded purchases red on game overview diff --git a/common/components.py b/common/components.py index 59db722..e1582e8 100644 --- a/common/components.py +++ b/common/components.py @@ -9,7 +9,7 @@ from django.urls import NoReverseMatch, reverse from django.utils.safestring import SafeText, mark_safe from common.utils import truncate -from games.models import Purchase +from games.models import Edition, Game, Purchase, Session HTMLAttribute = tuple[str, str | int | bool] HTMLTag = str @@ -32,7 +32,7 @@ def Component( attributesList = [f'{name}="{value}"' for name, value in attributes] # make attribute list into a string # and insert space between tag and attribute list - attributesBlob = f" {" ".join(attributesList)}" + attributesBlob = f" {' '.join(attributesList)}" tag: str = "" if tag_name != "": tag = f"<{tag_name}{attributesBlob}>{childrenBlob}" @@ -188,27 +188,6 @@ def Icon( return result -def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText: - link = reverse("view_game", args=[int(game_id)]) - a_content = Div( - [("class", "inline-flex gap-2 items-center")], - [ - Icon( - platform.icon, - [("title", platform.name)], - ), - PopoverTruncated(name), - ], - ) - - return mark_safe( - A( - url=link, - children=[a_content], - ), - ) - - def LinkedPurchase(purchase: Purchase) -> SafeText: link = reverse("view_purchase", args=[int(purchase.id)]) link_content = "" @@ -250,19 +229,63 @@ def LinkedPurchase(purchase: Purchase) -> SafeText: return mark_safe(A(url=link, children=[a_content])) -def NameWithPlatformIcon(name: str, platform: str) -> SafeText: +def NameWithIcon( + name: str = "", + platform: str = "", + game_id: int = 0, + session_id: int = 0, + purchase_id: int = 0, + edition_id: int = 0, + linkify: bool = True, + emulated: bool = False, +) -> SafeText: + create_link = False + link = "" + edition = None + platform = None + if ( + game_id != 0 or session_id != 0 or purchase_id != 0 or edition_id != 0 + ) and linkify: + create_link = True + if session_id: + session = Session.objects.get(pk=session_id) + emulated = session.emulated + edition = session.purchase.first_edition + game_id = edition.game.pk + if purchase_id: + purchase = Purchase.objects.get(pk=purchase_id) + edition = purchase.first_edition + game_id = purchase.edition.game.pk + if edition_id: + edition = Edition.objects.get(pk=edition_id) + game_id = edition.game.pk + if game_id: + game = Game.objects.get(pk=game_id) + name = edition.name if edition else game.name + platform = edition.platform if edition else None + link = reverse("view_game", args=[int(game_id)]) content = Div( [("class", "inline-flex gap-2 items-center")], [ Icon( platform.icon, [("title", platform.name)], - ), + ) + if platform + else "", + Icon("emulated", [("title", "Emulated")]) if emulated else "", PopoverTruncated(name), ], ) - return mark_safe(content) + return mark_safe( + A( + url=link, + children=[content], + ) + if create_link + else content, + ) def PurchasePrice(purchase) -> str: diff --git a/games/forms.py b/games/forms.py index 415b0cb..4793367 100644 --- a/games/forms.py +++ b/games/forms.py @@ -33,6 +33,7 @@ class SessionForm(forms.ModelForm): "timestamp_start", "timestamp_end", "duration_manual", + "emulated", "device", "note", ] diff --git a/games/migrations/0046_session_emulated.py b/games/migrations/0046_session_emulated.py new file mode 100644 index 0000000..65217db --- /dev/null +++ b/games/migrations/0046_session_emulated.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2025-01-29 11:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0045_alter_purchase_editions'), + ] + + operations = [ + migrations.AddField( + model_name='session', + name='emulated', + field=models.BooleanField(default=False), + ), + ] diff --git a/games/models.py b/games/models.py index 94323c6..017b02e 100644 --- a/games/models.py +++ b/games/models.py @@ -141,6 +141,10 @@ class Purchase(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) + @property + def standardized_name(self): + return self.name if self.name else self.first_edition.name + @property def first_edition(self): return self.editions.first() @@ -220,6 +224,8 @@ class Session(models.Model): default=None, ) note = models.TextField(blank=True, null=True) + emulated = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) diff --git a/games/static/base.css b/games/static/base.css index d02ca26..b27e1bf 100644 --- a/games/static/base.css +++ b/games/static/base.css @@ -1443,10 +1443,6 @@ input:checked + .toggle-bg { margin-top: 1rem; } -.ml-4 { - margin-left: 1rem; -} - .block { display: block; } @@ -1475,10 +1471,6 @@ input:checked + .toggle-bg { display: grid; } -.list-item { - display: list-item; -} - .hidden { display: none; } diff --git a/games/templates/cotton/icon/emulated.html b/games/templates/cotton/icon/emulated.html new file mode 100644 index 0000000..c873a9a --- /dev/null +++ b/games/templates/cotton/icon/emulated.html @@ -0,0 +1,6 @@ + + + + M 8.5 5 C 6.0324991 5 4 7.0324991 4 9.5 L 4 30.5 C 4 32.967501 6.0324991 35 8.5 35 L 17 35 L 17 40 L 13.5 40 A 1.50015 1.50015 0 1 0 13.5 43 L 18.253906 43 A 1.50015 1.50015 0 0 0 18.740234 43 L 29.253906 43 A 1.50015 1.50015 0 0 0 29.740234 43 L 34.5 43 A 1.50015 1.50015 0 1 0 34.5 40 L 31 40 L 31 35 L 39.5 35 C 41.967501 35 44 32.967501 44 30.5 L 44 9.5 C 44 7.0324991 41.967501 5 39.5 5 L 8.5 5 z M 8.5 8 L 39.5 8 C 40.346499 8 41 8.6535009 41 9.5 L 41 30.5 C 41 31.346499 40.346499 32 39.5 32 L 29.746094 32 A 1.50015 1.50015 0 0 0 29.259766 32 L 18.746094 32 A 1.50015 1.50015 0 0 0 18.259766 32 L 8.5 32 C 7.6535009 32 7 31.346499 7 30.5 L 7 9.5 C 7 8.6535009 7.6535009 8 8.5 8 z M 17.5 12 C 16.136406 12 15 13.136406 15 14.5 L 15 25.5 C 15 26.863594 16.136406 28 17.5 28 L 30.5 28 C 31.863594 28 33 26.863594 33 25.5 L 33 14.5 C 33 13.136406 31.863594 12 30.5 12 L 17.5 12 z M 18 18 L 30 18 L 30 25 L 18 25 L 18 18 z M 20 35 L 28 35 L 28 40 L 20 40 L 20 35 z + + diff --git a/games/views/edition.py b/games/views/edition.py index 6545821..ffb0039 100644 --- a/games/views/edition.py +++ b/games/views/edition.py @@ -11,7 +11,7 @@ from common.components import ( A, Button, Icon, - LinkedNameWithPlatformIcon, + NameWithIcon, PopoverTruncated, ) from common.time import dateformat, local_strftime @@ -54,11 +54,7 @@ def list_editions(request: HttpRequest) -> HttpResponse: ], "rows": [ [ - LinkedNameWithPlatformIcon( - name=edition.name, - game_id=edition.game.id, - platform=edition.platform, - ), + NameWithIcon(edition_id=edition.pk), PopoverTruncated( edition.name if edition.game.name != edition.name diff --git a/games/views/game.py b/games/views/game.py index 0aac65c..2f99716 100644 --- a/games/views/game.py +++ b/games/views/game.py @@ -14,7 +14,7 @@ from common.components import ( Div, Icon, LinkedPurchase, - NameWithPlatformIcon, + NameWithIcon, Popover, PopoverTruncated, PurchasePrice, @@ -67,18 +67,7 @@ def list_games(request: HttpRequest) -> HttpResponse: ], "rows": [ [ - A( - [ - ( - "href", - reverse( - "view_game", - args=[game.pk], - ), - ) - ], - PopoverTruncated(game.name), - ), + NameWithIcon(game_id=game.pk), PopoverTruncated( game.sort_name if game.sort_name is not None and game.name != game.sort_name @@ -212,10 +201,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: ], "rows": [ [ - NameWithPlatformIcon( - name=edition.name, - platform=edition.platform, - ), + NameWithIcon(edition_id=edition.pk), edition.year_released, render_to_string( "cotton/button_group.html", @@ -321,13 +307,10 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse: "columns": ["Edition", "Date", "Duration", "Actions"], "rows": [ [ - NameWithPlatformIcon( - name=session.purchase.name - if session.purchase.name - else session.purchase.first_edition.name, - platform=session.purchase.platform, + NameWithIcon( + session_id=session.pk, ), - f"{local_strftime(session.timestamp_start)}{f" — {local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}", + f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}", ( format_duration(session.duration_calculated, durationformat) if session.duration_calculated diff --git a/games/views/session.py b/games/views/session.py index f886a9b..b84e032 100644 --- a/games/views/session.py +++ b/games/views/session.py @@ -15,7 +15,7 @@ from common.components import ( Div, Form, Icon, - LinkedNameWithPlatformIcon, + NameWithIcon, Popover, ) from common.time import ( @@ -130,12 +130,8 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse ], "rows": [ [ - LinkedNameWithPlatformIcon( - name=session.purchase.first_edition.name, - game_id=session.purchase.first_edition.game.pk, - platform=session.purchase.platform, - ), - f"{local_strftime(session.timestamp_start)}{f" — {local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}", + NameWithIcon(session_id=session.pk), + f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}", ( format_duration(session.duration_calculated, durationformat) if session.duration_calculated