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}{tag_name}>"
@@ -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