From 7157f3b0b05789a84210b4c54605de38f7a62fec Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 15 Jun 2024 18:26:20 -0400 Subject: [PATCH] add progress to note --- catalog/templates/_item_user_pieces.html | 31 +++-- common/forms.py | 12 ++ common/static/scss/_dialog.scss | 10 ++ common/static/scss/_lightbox.scss | 26 ++++ common/static/scss/_post.scss | 7 ++ common/static/scss/neodb.scss | 1 + journal/migrations/0001_initial_0_10.py | 132 ++++++++++++++++++-- journal/migrations/0002_note.py | 12 +- journal/migrations/0003_note_progress.py | 44 +++++++ journal/models/common.py | 4 +- journal/models/itemlist.py | 10 +- journal/models/like.py | 6 +- journal/models/mark.py | 4 +- journal/models/note.py | 152 +++++++++++++++++++++++ journal/templates/note.html | 55 +++----- journal/templates/posts.html | 14 ++- journal/urls.py | 4 +- journal/views/__init__.py | 3 +- journal/views/mark.py | 52 +------- journal/views/note.py | 110 ++++++++++++++++ locale/zh_Hans/LC_MESSAGES/django.po | 141 ++++++++++++++++----- locale/zh_Hant/LC_MESSAGES/django.po | 141 ++++++++++++++++----- 22 files changed, 779 insertions(+), 192 deletions(-) create mode 100644 common/static/scss/_lightbox.scss create mode 100644 journal/migrations/0003_note_progress.py create mode 100644 journal/views/note.py diff --git a/catalog/templates/_item_user_pieces.html b/catalog/templates/_item_user_pieces.html index 4e03832d..7b38541a 100644 --- a/catalog/templates/_item_user_pieces.html +++ b/catalog/templates/_item_user_pieces.html @@ -90,18 +90,25 @@ {% include "action_open_post.html" with post=note.latest_post %} {% endif %} -
{{ note.title|default:'' }}
-

{{ note.content|linebreaks }}

-
- {% for attachment in note.attachments %} - {% if attachment.type == 'image' %} - image attachment - {% endif %} - {% endfor %} -
+ {% if note.title %}
{{ note.title|default:'' }}
{% endif %} +

+ {% if note.progress_value %}{{ note.progress_display }}{% endif %} + {{ note.content|linebreaksbr }} +

+ {% for attachment in note.attachments %} + {% if attachment.type == 'image' %} + + image attachment + + + + + {% endif %} + {% endfor %} +
+

{% endfor %}
diff --git a/common/forms.py b/common/forms.py index 3ae7c39c..a9ef67fd 100644 --- a/common/forms.py +++ b/common/forms.py @@ -3,11 +3,23 @@ import json import django.contrib.postgres.forms as postgres from django import forms from django.core.exceptions import ValidationError +from django.forms import ModelForm from django.utils import formats from django.utils.translation import gettext_lazy as _ from markdownx.fields import MarkdownxFormField +class NeoModelForm(ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # if "uuid" in self.fields: + # if self.instance and self.instance.pk: + # self.fields["uuid"].initial = self.instance.uuid + for visible in self.visible_fields(): + w = visible.field.widget + w.attrs["class"] = "widget " + w.__class__.__name__.lower() + + class PreviewImageInput(forms.FileInput): template_name = "widgets/image.html" diff --git a/common/static/scss/_dialog.scss b/common/static/scss/_dialog.scss index a13f7992..e7274349 100644 --- a/common/static/scss/_dialog.scss +++ b/common/static/scss/_dialog.scss @@ -86,6 +86,16 @@ dialog { article { padding-bottom: 1em; + form { + overflow: hidden; + input[type="submit"] { + margin-top: 1em; + } + div.widget.radioselect { + display: flex; + grid-column-gap: 1em; + } + } } input[type="date"] { diff --git a/common/static/scss/_lightbox.scss b/common/static/scss/_lightbox.scss new file mode 100644 index 00000000..4f4efd88 --- /dev/null +++ b/common/static/scss/_lightbox.scss @@ -0,0 +1,26 @@ +.lightbox { + display: none; + position: fixed; + z-index: 999; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding: 1em; + background: rgba(0, 0, 0, 0.8); + -webkit-backdrop-filter: var(--pico-modal-overlay-backdrop-filter); +} + +.lightbox:target { + display: block; +} + +.lightbox span { + display: block; + width: 100%; + height: 100%; + + background-position: center; + background-repeat: no-repeat; + background-size: contain; +} diff --git a/common/static/scss/_post.scss b/common/static/scss/_post.scss index ba45b7d1..5bba7e20 100644 --- a/common/static/scss/_post.scss +++ b/common/static/scss/_post.scss @@ -27,3 +27,10 @@ section.replies { } } } + +.attachements { + img.preview { + max-height: 6em; + max-width: 50%; + } +} diff --git a/common/static/scss/neodb.scss b/common/static/scss/neodb.scss index eb8afbb0..7e031ea4 100644 --- a/common/static/scss/neodb.scss +++ b/common/static/scss/neodb.scss @@ -20,3 +20,4 @@ @import '_form.scss'; @import '_post.scss'; @import '_l10n.scss'; +@import '_lightbox.scss'; diff --git a/journal/migrations/0001_initial_0_10.py b/journal/migrations/0001_initial_0_10.py index aa7e623d..fbbfa121 100644 --- a/journal/migrations/0001_initial_0_10.py +++ b/journal/migrations/0001_initial_0_10.py @@ -159,7 +159,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -205,7 +215,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -233,7 +253,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -265,7 +295,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -316,7 +356,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -343,7 +393,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -382,7 +442,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -415,7 +485,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -451,7 +531,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -596,7 +686,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), @@ -656,7 +756,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), diff --git a/journal/migrations/0002_note.py b/journal/migrations/0002_note.py index da630bf7..131b4bc5 100644 --- a/journal/migrations/0002_note.py +++ b/journal/migrations/0002_note.py @@ -27,7 +27,17 @@ class Migration(migrations.Migration): to="journal.piece", ), ), - ("visibility", models.PositiveSmallIntegerField(default=0)), + ( + "visibility", + models.PositiveSmallIntegerField( + choices=[ + (0, "Public"), + (1, "Followers Only"), + (2, "Mentioned Only"), + ], + default=0, + ), + ), ( "created_time", models.DateTimeField(default=django.utils.timezone.now), diff --git a/journal/migrations/0003_note_progress.py b/journal/migrations/0003_note_progress.py new file mode 100644 index 00000000..13c1e770 --- /dev/null +++ b/journal/migrations/0003_note_progress.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.13 on 2024-06-14 22:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("journal", "0002_note"), + ] + + operations = [ + migrations.AddField( + model_name="note", + name="progress_type", + field=models.CharField( + blank=True, + choices=[ + ("page", "Page"), + ("chapter", "Chapter"), + ("part", "Part"), + ("episode", "Episode"), + ("track", "Track"), + ("cycle", "Cycle"), + ("timestamp", "Timestamp"), + ("percentage", "Percentage"), + ], + default=None, + max_length=50, + null=True, + ), + ), + migrations.AddField( + model_name="note", + name="progress_value", + field=models.CharField(blank=True, default=None, max_length=500, null=True), + ), + migrations.AddIndex( + model_name="note", + index=models.Index( + fields=["owner", "item", "created_time"], + name="journal_not_owner_i_7f9460_idx", + ), + ), + ] diff --git a/journal/models/common.py b/journal/models/common.py index 375198c6..da485e7b 100644 --- a/journal/models/common.py +++ b/journal/models/common.py @@ -434,8 +434,8 @@ class PieceInteraction(models.Model): class Content(Piece): owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT) visibility = models.PositiveSmallIntegerField( - default=0 - ) # 0: Public / 1: Follower only / 2: Self only # type:ignore + choices=VisibilityType.choices, default=0, null=False + ) # type:ignore created_time = models.DateTimeField(default=timezone.now) edited_time = models.DateTimeField(auto_now=True) metadata = models.JSONField(default=dict) diff --git a/journal/models/itemlist.py b/journal/models/itemlist.py index 41e6ecae..a29c815e 100644 --- a/journal/models/itemlist.py +++ b/journal/models/itemlist.py @@ -8,7 +8,7 @@ from django.utils import timezone from catalog.models import Item, ItemCategory from users.models import APIdentity -from .common import Piece +from .common import Piece, VisibilityType list_add = django.dispatch.Signal() list_remove = django.dispatch.Signal() @@ -25,8 +25,8 @@ class List(Piece): items: "models.ManyToManyField[Item, List]" owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT) visibility = models.PositiveSmallIntegerField( - default=0 - ) # 0: Public / 1: Follower only / 2: Self only # type:ignore + choices=VisibilityType.choices, default=0, null=False + ) # type:ignore created_time = models.DateTimeField(default=timezone.now) edited_time = models.DateTimeField(auto_now=True) metadata = models.JSONField(default=dict) @@ -151,8 +151,8 @@ class ListMember(Piece): parent: models.ForeignKey["ListMember", "List"] owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT) visibility = models.PositiveSmallIntegerField( - default=0 - ) # 0: Public / 1: Follower only / 2: Self only # type:ignore[reportAssignmentType] + choices=VisibilityType.choices, default=0, null=False + ) # type:ignore created_time = models.DateTimeField(default=timezone.now) edited_time = models.DateTimeField(auto_now=True) metadata = models.JSONField(default=dict) diff --git a/journal/models/like.py b/journal/models/like.py index de34ffaa..9e669e55 100644 --- a/journal/models/like.py +++ b/journal/models/like.py @@ -5,14 +5,14 @@ from django.utils.translation import gettext_lazy as _ from users.models import APIdentity -from .common import Piece +from .common import Piece, VisibilityType class Like(Piece): # TODO remove owner = models.ForeignKey(APIdentity, on_delete=models.PROTECT) visibility = models.PositiveSmallIntegerField( - default=0 - ) # 0: Public / 1: Follower only / 2: Self only # type: ignore + choices=VisibilityType.choices, default=0, null=False + ) # type:ignore created_time = models.DateTimeField(default=timezone.now) edited_time = models.DateTimeField(auto_now=True) target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name="likes") diff --git a/journal/models/mark.py b/journal/models/mark.py index 9b7cb88d..5f97d44e 100644 --- a/journal/models/mark.py +++ b/journal/models/mark.py @@ -108,7 +108,9 @@ class Mark: @cached_property def notes(self): - return Note.objects.filter(owner=self.owner, item=self.item) + return Note.objects.filter(owner=self.owner, item=self.item).order_by( + "-created_time" + ) # post_ids = PiecePost.objects.filter( # piece__note__owner_id=self.owner.pk, piece__note__item_id=self.item.pk # ).values_list("post_id", flat=True) diff --git a/journal/models/note.py b/journal/models/note.py index cf84837b..9011ae30 100644 --- a/journal/models/note.py +++ b/journal/models/note.py @@ -1,9 +1,12 @@ +import re from functools import cached_property from typing import override from django.db import models +from django.utils.translation import gettext_lazy as _ from loguru import logger +from catalog.models import Item from mastodon.api import delete_toot_later from takahe.utils import Takahe @@ -11,17 +14,74 @@ from .common import Content from .renderers import render_text from .shelf import ShelfMember +_progress = re.compile( + r"^\s*(?P(p|pg|page|ch|chapter|pt|part|e|ep|episode|trk|track|cycle))?(\s|\.|#)*(?P(\d[\d\:\.\-]*\d|\d))\s*(?P(%))?\s*(\s|\n|\.|:)", + re.IGNORECASE, +) + +_number = re.compile(r"^[\s\d\:\.]+$") + class Note(Content): + class ProgressType(models.TextChoices): + PAGE = "page", _("Page") + CHAPTER = "chapter", _("Chapter") + # SECTION = "section", _("Section") + # VOLUME = "volume", _("Volume") + PART = "part", _("Part") + EPISODE = "episode", _("Episode") + TRACK = "track", _("Track") + CYCLE = "cycle", _("Cycle") + TIMESTAMP = "timestamp", _("Timestamp") + PERCENTAGE = "percentage", _("Percentage") + title = models.TextField(blank=True, null=True, default=None) content = models.TextField(blank=False, null=False) sensitive = models.BooleanField(default=False, null=False) attachments = models.JSONField(default=list) + progress_type = models.CharField( + max_length=50, + choices=ProgressType.choices, + blank=True, + null=True, + default=None, + ) + progress_value = models.CharField( + max_length=500, blank=True, null=True, default=None + ) + _progress_display_template = { + ProgressType.PAGE: _("Page {value}"), + ProgressType.CHAPTER: _("Chapter {value}"), + # ProgressType.SECTION: _("Section {value}"), + # ProgressType.VOLUME: _("Volume {value}"), + ProgressType.PART: _("Part {value}"), + ProgressType.EPISODE: _("Episode {value}"), + ProgressType.TRACK: _("Track {value}"), + ProgressType.CYCLE: _("Cycle {value}"), + ProgressType.PERCENTAGE: "{value}%", + ProgressType.TIMESTAMP: "{value}", + } + + class Meta: + indexes = [models.Index(fields=["owner", "item", "created_time"])] @property def html(self): return render_text(self.content) + @property + def progress_display(self) -> str: + if not self.progress_value: + return "" + if not self.progress_type: + return str(self.progress_value) + tpl = Note._progress_display_template.get(self.progress_type, None) + if not tpl: + return str(self.progress_value) + if _number.match(self.progress_value): + return tpl.format(value=self.progress_value) + return self.progress_type.display + ": " + self.progress_value + @property def ap_object(self): d = { @@ -36,6 +96,11 @@ class Note(Content): "withRegardTo": self.item.absolute_url, "href": self.absolute_url, } + if self.progress_value: + d["progress"] = { + "type": self.progress_type, + "value": self.progress_value, + } return d @override @@ -47,6 +112,17 @@ class Note(Content): "sensitive": obj.get("sensitive", post.sensitive), "attachments": [], } + progress = obj.get("progress", {}) + if progress.get("type"): + params["progress_type"] = progress.get("type") + if progress.get("value"): + params["progress_value"] = progress.get("value") + if post.local: + progress_type, progress_value = cls.extract_progress(params["content"]) + print(progress_type, progress_value) + if progress_value: + params["progress_type"] = progress_type + params["progress_value"] = progress_value if post: for atta in post.attachments.all(): params["attachments"].append( @@ -103,3 +179,79 @@ class Note(Content): ), # not passing "attachments" so it won't change } + + @classmethod + def extract_progress(cls, content): + m = _progress.match(content) + if m and m["value"]: + typ_ = "percentage" if m["postfix"] == "%" else m["prefix"] + match typ_: + case "p" | "pg" | "page": + typ = Note.ProgressType.PAGE + case "ch" | "chapter": + typ = Note.ProgressType.CHAPTER + # case "vol" | "volume": + # typ = ProgressType.VOLUME + # case "section": + # typ = ProgressType.SECTION + case "pt" | "part": + typ = Note.ProgressType.PART + case "e" | "ep" | "episode": + typ = Note.ProgressType.EPISODE + case "trk" | "track": + typ = Note.ProgressType.TRACK + case "cycle": + typ = Note.ProgressType.CYCLE + case "percentage": + typ = Note.ProgressType.PERCENTAGE + case _: + typ = "timestamp" if ":" in m["value"] else None + return typ, m["value"] + return None, None + + @classmethod + def get_progress_types_by_item(cls, item: Item): + match item.__class__.__name__: + case "Edition": + v = [ + Note.ProgressType.PAGE, + Note.ProgressType.CHAPTER, + Note.ProgressType.PERCENTAGE, + ] + case "TVShow" | "TVSeason": + v = [ + Note.ProgressType.PART, + Note.ProgressType.EPISODE, + Note.ProgressType.PERCENTAGE, + ] + case "Movie": + v = [ + Note.ProgressType.PART, + Note.ProgressType.TIMESTAMP, + Note.ProgressType.PERCENTAGE, + ] + case "Podcast": + v = [ + Note.ProgressType.EPISODE, + ] + case "TVEpisode" | "PodcastEpisode": + v = [] + case "Album": + v = [ + Note.ProgressType.TRACK, + Note.ProgressType.TIMESTAMP, + Note.ProgressType.PERCENTAGE, + ] + case "Game": + v = [ + Note.ProgressType.CYCLE, + ] + case "Performance" | "PerformanceProduction": + v = [ + Note.ProgressType.PART, + Note.ProgressType.TIMESTAMP, + Note.ProgressType.PERCENTAGE, + ] + case _: + v = [] + return v diff --git a/journal/templates/note.html b/journal/templates/note.html index 88085cd0..91ab9588 100644 --- a/journal/templates/note.html +++ b/journal/templates/note.html @@ -16,50 +16,23 @@ {% trans 'Note' %} - {{ item.display_title }}
-
+ {% csrf_token %} - - + {{ form.uuid }}
+
{{ form.progress_type }}
+
{{ form.progress_value }}
+
+ {{ form.content }} + {{ form.title }} +
+
{{ form.visibility }}
-
- - - - - - -
-
-
-
- {% if request.user.mastodon_acct %} - - {% endif %} -
+
diff --git a/journal/templates/posts.html b/journal/templates/posts.html index e7bffa91..c88b46a6 100644 --- a/journal/templates/posts.html +++ b/journal/templates/posts.html @@ -25,13 +25,17 @@
{{ post.summary|default:'' }}
-
+
{% for attachment in post.attachments.all %} {% if attachment.is_image %} - attachment.file_display_name + + image attachment + + + + {% endif %} {% endfor %}
diff --git a/journal/urls.py b/journal/urls.py index 2fd9db3a..a568d2c3 100644 --- a/journal/urls.py +++ b/journal/urls.py @@ -21,8 +21,8 @@ urlpatterns = [ path("wish/", wish, name="wish"), path("mark/", mark, name="mark"), path("comment/", comment, name="comment"), - path("note/", note, name="note"), - path("note//", note, name="note"), + path("item//note", note_edit, name="note"), + path("item//note/", note_edit, name="note"), path("piece//replies", piece_replies, name="piece_replies"), path("post//replies", post_replies, name="post_replies"), path("post//reply", post_reply, name="post_reply"), diff --git a/journal/views/__init__.py b/journal/views/__init__.py index 4c6b754b..5774c91f 100644 --- a/journal/views/__init__.py +++ b/journal/views/__init__.py @@ -16,7 +16,8 @@ from .collection import ( user_liked_collection_list, ) from .common import piece_delete -from .mark import comment, mark, mark_log, note, user_mark_list, wish +from .mark import comment, mark, mark_log, user_mark_list, wish +from .note import note_edit from .post import ( piece_replies, post_boost, diff --git a/journal/views/mark.py b/journal/views/mark.py index d70018f4..0b010e53 100644 --- a/journal/views/mark.py +++ b/journal/views/mark.py @@ -1,5 +1,6 @@ from datetime import datetime +from django import forms from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied @@ -13,10 +14,8 @@ from loguru import logger from catalog.models import * from common.utils import AuthedHttpRequest, get_uuid_or_404 -from mastodon.api import boost_toot_later -from takahe.utils import Takahe -from ..models import Comment, Mark, Note, ShelfManager, ShelfType, TagManager +from ..models import Comment, Mark, ShelfManager, ShelfType, TagManager from .common import render_list, render_relogin, target_identity_required PAGE_SIZE = 10 @@ -190,53 +189,6 @@ def comment(request: AuthedHttpRequest, item_uuid): return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) -@login_required -@require_http_methods(["GET", "POST"]) -def note(request: AuthedHttpRequest, item_uuid: str, note_uuid: str = ""): - item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) - note_uuid = request.POST.get("uuid", note_uuid) - note = None - content = request.POST.get("content") - if note_uuid: - note = get_object_or_404( - Note, owner=request.user.identity, item=item, uid=get_uuid_or_404(note_uuid) - ) - if request.method == "GET": - return render( - request, - "note.html", - { - "item": item, - "note": note, - }, - ) - else: - if request.POST.get("delete", default=False) or not content: - if not note: - raise Http404(_("Content not found")) - note.delete() - return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) - share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False)) - visibility = int(request.POST.get("visibility", default=0)) - delete_existing_post = False - if note: - delete_existing_post = visibility != note.visibility - note.content = content - note.visibility = visibility - note.save() - else: - note = Note.objects.create( - owner=request.user.identity, - item=item, - content=content, - visibility=visibility, - ) - note.sync_to_timeline(delete_existing=delete_existing_post) - if share_to_mastodon: - note.sync_to_mastodon(delete_existing=delete_existing_post) - return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) - - def user_mark_list(request: AuthedHttpRequest, user_name, shelf_type, item_category): return render_list( request, user_name, "mark", shelf_type=shelf_type, item_category=item_category diff --git a/journal/views/note.py b/journal/views/note.py new file mode 100644 index 00000000..d6a1c72f --- /dev/null +++ b/journal/views/note.py @@ -0,0 +1,110 @@ +from django import forms +from django.contrib.auth.decorators import login_required +from django.core.exceptions import BadRequest, ObjectDoesNotExist, PermissionDenied +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.translation import gettext as _ +from django.views.decorators.http import require_http_methods + +from catalog.models import Item +from common.forms import NeoModelForm +from common.utils import AuthedHttpRequest, get_uuid_or_404 + +from ..models import Note +from ..models.common import VisibilityType + + +class NoteForm(NeoModelForm): + # _progress_choices = [ + # ("", _("Progress Type (optional)")) + # ] + Note.ProgressType.choices + # progress_type = forms.ChoiceField(choices=_progress_choices, required=False) + visibility = forms.ChoiceField( + widget=forms.RadioSelect(), choices=VisibilityType.choices, initial=0 + ) + share_to_mastodon = forms.BooleanField( + label=_("Post to Fediverse"), initial=True, required=False + ) + uuid = forms.CharField(widget=forms.HiddenInput(), required=False) + # content = forms.CharField(required=False, widget=forms.Textarea) + + class Meta: + model = Note + fields = [ + "id", + "title", + "content", + "visibility", + "progress_type", + "progress_value", + "sensitive", + ] + widgets = { + "progress_value": forms.TextInput( + attrs={"placeholder": _("Progress (optional)")} + ), + "content": forms.Textarea(attrs={"placeholder": _("Note Content")}), + "title": forms.TextInput( + attrs={"placeholder": _("Content Warning (optional)")} + ), + } + + def __init__(self, *args, **kwargs): + item = kwargs.pop("item") + super().__init__(*args, **kwargs) + # allow submit empty content for existing note, and we'll delete it + if self.instance.id: + self.fields["content"].required = False + # get the corresponding progress types for the item + types = Note.get_progress_types_by_item(item) + if self.instance.progress_type and self.instance.progress_type not in types: + types.append(self.instance.progress_type) + choices = [("", _("Progress Type (optional)"))] + [(x, x.label) for x in types] + self.fields["progress_type"].choices = choices # type: ignore + + +@login_required +@require_http_methods(["GET", "POST"]) +def note_edit(request: AuthedHttpRequest, item_uuid: str, note_uuid: str = ""): + item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid)) + owner = request.user.identity + note_uuid = request.POST.get("uuid", note_uuid) + note = None + if note_uuid: + note = get_object_or_404( + Note, owner=owner, item=item, uid=get_uuid_or_404(note_uuid) + ) + form = NoteForm( + request.POST or None, item=item, instance=note, initial={"uuid": note_uuid} + ) + form.instance.owner = owner + form.instance.item = item + if request.method == "GET": + return render( + request, + "note.html", + { + "item": item, + "note": note, + "form": form, + }, + ) + if not form.data["content"]: + if not note: + raise Http404(_("Content not found")) + note.delete() + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + if note: + orig_visibility = note.visibility + else: + orig_visibility = None + if not form.is_valid(): + raise BadRequest(_("Invalid form data")) + note = form.save() + delete_existing_post = ( + orig_visibility is not None and orig_visibility != note.visibility + ) + note.sync_to_timeline(delete_existing=delete_existing_post) + if form.cleaned_data["share_to_mastodon"]: + note.sync_to_mastodon(delete_existing=delete_existing_post) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) diff --git a/locale/zh_Hans/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po index be6fa88f..7ebaece9 100644 --- a/locale/zh_Hans/LC_MESSAGES/django.po +++ b/locale/zh_Hans/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-13 20:50-0400\n" +"POT-Creation-Date: 2024-06-15 18:22-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -715,15 +715,15 @@ msgstr "我的短评和标签" msgid "my notes" msgstr "我的笔记" -#: catalog/templates/_item_user_pieces.html:101 +#: catalog/templates/_item_user_pieces.html:118 msgid "my review" msgstr "我的评论" -#: catalog/templates/_item_user_pieces.html:143 +#: catalog/templates/_item_user_pieces.html:160 msgid "my collection" msgstr "我的收藏单" -#: catalog/templates/_item_user_pieces.html:173 +#: catalog/templates/_item_user_pieces.html:190 msgid "mark history" msgstr "标记历史" @@ -1029,7 +1029,7 @@ msgstr "创建" #: catalog/templates/catalog_edit.html:50 #: journal/templates/add_to_collection.html:35 #: journal/templates/collection_edit.html:38 journal/templates/comment.html:69 -#: journal/templates/mark.html:147 journal/templates/note.html:66 +#: journal/templates/mark.html:147 journal/templates/note.html:39 #: journal/templates/review_edit.html:39 journal/templates/tag_edit.html:51 #: users/templates/users/account.html:43 users/templates/users/account.html:104 #: users/templates/users/preferences.html:168 @@ -1114,7 +1114,7 @@ msgstr "热门标签" #: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:99 #: common/templates/_sidebar_anonymous.html:43 #: common/templates/_sidebar_anonymous.html:58 -#: journal/templates/collection_items.html:8 journal/templates/posts.html:45 +#: journal/templates/collection_items.html:8 journal/templates/posts.html:49 #: journal/templates/profile.html:109 journal/templates/profile.html:151 #: journal/templates/profile.html:187 #: journal/templates/user_collection_list.html:51 @@ -1393,7 +1393,7 @@ msgstr "权限不足" #: catalog/views_edit.py:200 journal/views/collection.py:229 #: journal/views/collection.py:296 journal/views/common.py:81 -#: journal/views/mark.py:142 journal/views/post.py:41 journal/views/post.py:55 +#: journal/views/mark.py:141 journal/views/post.py:41 journal/views/post.py:55 #: journal/views/review.py:93 journal/views/review.py:96 users/views.py:169 msgid "Invalid parameter" msgstr "无效参数" @@ -1678,7 +1678,7 @@ msgstr "标题" msgid "Content (Markdown)" msgstr "内容 (Markdown格式)" -#: journal/forms.py:21 +#: journal/forms.py:21 journal/views/note.py:26 msgid "Post to Fediverse" msgstr "发布到联邦宇宙" @@ -1722,31 +1722,96 @@ msgstr "备注" #: journal/templates/action_open_post.html:14 #: journal/templates/action_open_post.html:16 #: journal/templates/collection_share.html:35 journal/templates/comment.html:35 -#: journal/templates/mark.html:93 journal/templates/note.html:32 -#: journal/templates/tag_edit.html:42 journal/templates/wrapped_share.html:43 -#: users/templates/users/data.html:47 users/templates/users/data.html:139 +#: journal/templates/mark.html:93 journal/templates/tag_edit.html:42 +#: journal/templates/wrapped_share.html:43 users/templates/users/data.html:47 +#: users/templates/users/data.html:139 #: users/templates/users/preferences.html:54 msgid "Public" msgstr "公开" #: journal/models/common.py:34 journal/templates/action_open_post.html:10 #: journal/templates/collection_share.html:46 journal/templates/comment.html:42 -#: journal/templates/mark.html:100 journal/templates/note.html:39 -#: journal/templates/wrapped_share.html:49 users/templates/users/data.html:55 -#: users/templates/users/data.html:147 +#: journal/templates/mark.html:100 journal/templates/wrapped_share.html:49 +#: users/templates/users/data.html:55 users/templates/users/data.html:147 #: users/templates/users/preferences.html:61 msgid "Followers Only" msgstr "仅关注者" #: journal/models/common.py:35 journal/templates/action_open_post.html:12 #: journal/templates/collection_share.html:57 journal/templates/comment.html:49 -#: journal/templates/mark.html:107 journal/templates/note.html:46 -#: journal/templates/wrapped_share.html:55 users/templates/users/data.html:63 -#: users/templates/users/data.html:155 +#: journal/templates/mark.html:107 journal/templates/wrapped_share.html:55 +#: users/templates/users/data.html:63 users/templates/users/data.html:155 #: users/templates/users/preferences.html:68 msgid "Mentioned Only" msgstr "自己和提到的人" +#: journal/models/note.py:27 +msgid "Page" +msgstr "页码" + +#: journal/models/note.py:28 +msgid "Chapter" +msgstr "章节" + +#: journal/models/note.py:31 +msgid "Part" +msgstr "分部" + +#: journal/models/note.py:32 +msgid "Episode" +msgstr "单集" + +#: journal/models/note.py:33 +msgid "Track" +msgstr "曲目" + +#: journal/models/note.py:34 +msgid "Cycle" +msgstr "周目" + +#: journal/models/note.py:35 +msgid "Timestamp" +msgstr "时间戳" + +#: journal/models/note.py:36 +msgid "Percentage" +msgstr "百分比" + +#: journal/models/note.py:53 +#, python-brace-format +msgid "Page {value}" +msgstr "第{value}页" + +#: journal/models/note.py:54 +#, python-brace-format +msgid "Chapter {value}" +msgstr "第{value}章" + +#: journal/models/note.py:57 +#, python-brace-format +msgid "Part {value}" +msgstr "第{value}部" + +#: journal/models/note.py:58 +#, python-brace-format +msgid "Episode {value}" +msgstr "第{value}集" + +#: journal/models/note.py:59 +#, python-brace-format +msgid "Track {value}" +msgstr "第{value}首" + +#: journal/models/note.py:60 +#, python-brace-format +msgid "Cycle {value}" +msgstr "{value}周目" + +#: journal/models/renderers.py:94 mastodon/api.py:619 takahe/utils.py:540 +#, python-brace-format +msgid "regarding {item_title}, may contain spoiler or triggering content" +msgstr "关于 {item_title},可能包含剧透或敏感内容" + #: journal/models/shelf.py:24 msgid "WISHLIST" msgstr "" @@ -2323,7 +2388,6 @@ msgid "Tips: use >!text!< for spoilers; some instances may not be able to msgstr "提示: 善用 >!文字!< 标记可隐藏剧透; 超过360字可能无法分享到联邦宇宙实例时间轴。" #: journal/templates/comment.html:62 journal/templates/mark.html:120 -#: journal/templates/note.html:59 msgid "Repost to timeline" msgstr "转发到时间轴" @@ -2450,6 +2514,10 @@ msgstr "" msgid "Note" msgstr "笔记" +#: journal/templates/note.html:21 +msgid "Note with empty content will be deleted, sure to continue?" +msgstr "" + #: journal/templates/profile.html:55 msgid "calendar" msgstr "日历" @@ -2458,7 +2526,7 @@ msgstr "日历" msgid "annual summary" msgstr "年度小结" -#: journal/templates/profile.html:131 mastodon/api.py:747 +#: journal/templates/profile.html:131 mastodon/api.py:678 msgid "collection" msgstr "收藏单" @@ -2590,7 +2658,7 @@ msgstr "找不到条目,请使用本站条目网址。" msgid "Login required" msgstr "登录后访问" -#: journal/views/common.py:33 journal/views/mark.py:119 +#: journal/views/common.py:33 journal/views/mark.py:118 msgid "Data saved but unable to repost to Fediverse instance." msgstr "数据已保存但未能转发到联邦实例。" @@ -2602,15 +2670,35 @@ msgstr "正在重定向到你的联邦实例以重新认证。" msgid "List not found." msgstr "列表未找到" -#: journal/views/mark.py:110 +#: journal/views/mark.py:109 msgid "Content too long for your Fediverse instance." msgstr "内容过长,超出了你的联邦实例的限制。" -#: journal/views/mark.py:164 journal/views/mark.py:218 +#: journal/views/mark.py:163 journal/views/note.py:94 #: journal/views/review.py:30 msgid "Content not found" msgstr "内容未找到" +#: journal/views/note.py:44 +msgid "Progress (optional)" +msgstr "进度(选填)" + +#: journal/views/note.py:46 +msgid "Note Content" +msgstr "笔记内容" + +#: journal/views/note.py:48 +msgid "Content Warning (optional)" +msgstr "剧透或敏感内容提示(选填)" + +#: journal/views/note.py:62 +msgid "Progress Type (optional)" +msgstr "进度类型(选填)" + +#: journal/views/note.py:102 +msgid "Invalid form data" +msgstr "无效表单信息。" + #: journal/views/review.py:112 journal/views/review.py:126 #, python-brace-format msgid "Reviews by {0}" @@ -2649,16 +2737,11 @@ msgstr "标签已更新" msgid "Summary posted to timeline." msgstr "总结已发布到时间轴" -#: mastodon/api.py:600 takahe/utils.py:540 -#, python-brace-format -msgid "regarding {item_title}, may contain spoiler or triggering content" -msgstr "关于 {item_title},可能包含剧透或敏感内容" - -#: mastodon/api.py:752 +#: mastodon/api.py:683 msgid "shared my collection" msgstr "分享我的收藏单" -#: mastodon/api.py:755 +#: mastodon/api.py:686 #, python-brace-format msgid "shared {username}'s collection" msgstr "分享 {username} 的收藏单" diff --git a/locale/zh_Hant/LC_MESSAGES/django.po b/locale/zh_Hant/LC_MESSAGES/django.po index 74e3b91b..a9e66ebc 100644 --- a/locale/zh_Hant/LC_MESSAGES/django.po +++ b/locale/zh_Hant/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-13 20:50-0400\n" +"POT-Creation-Date: 2024-06-15 18:22-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -715,15 +715,15 @@ msgstr "我的短評和標籤" msgid "my notes" msgstr "我的筆記" -#: catalog/templates/_item_user_pieces.html:101 +#: catalog/templates/_item_user_pieces.html:118 msgid "my review" msgstr "我的評論" -#: catalog/templates/_item_user_pieces.html:143 +#: catalog/templates/_item_user_pieces.html:160 msgid "my collection" msgstr "我的收藏單" -#: catalog/templates/_item_user_pieces.html:173 +#: catalog/templates/_item_user_pieces.html:190 msgid "mark history" msgstr "標記歷史" @@ -1029,7 +1029,7 @@ msgstr "創建" #: catalog/templates/catalog_edit.html:50 #: journal/templates/add_to_collection.html:35 #: journal/templates/collection_edit.html:38 journal/templates/comment.html:69 -#: journal/templates/mark.html:147 journal/templates/note.html:66 +#: journal/templates/mark.html:147 journal/templates/note.html:39 #: journal/templates/review_edit.html:39 journal/templates/tag_edit.html:51 #: users/templates/users/account.html:43 users/templates/users/account.html:104 #: users/templates/users/preferences.html:168 @@ -1114,7 +1114,7 @@ msgstr "熱門標籤" #: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:99 #: common/templates/_sidebar_anonymous.html:43 #: common/templates/_sidebar_anonymous.html:58 -#: journal/templates/collection_items.html:8 journal/templates/posts.html:45 +#: journal/templates/collection_items.html:8 journal/templates/posts.html:49 #: journal/templates/profile.html:109 journal/templates/profile.html:151 #: journal/templates/profile.html:187 #: journal/templates/user_collection_list.html:51 @@ -1393,7 +1393,7 @@ msgstr "權限不足" #: catalog/views_edit.py:200 journal/views/collection.py:229 #: journal/views/collection.py:296 journal/views/common.py:81 -#: journal/views/mark.py:142 journal/views/post.py:41 journal/views/post.py:55 +#: journal/views/mark.py:141 journal/views/post.py:41 journal/views/post.py:55 #: journal/views/review.py:93 journal/views/review.py:96 users/views.py:169 msgid "Invalid parameter" msgstr "無效參數" @@ -1678,7 +1678,7 @@ msgstr "標題" msgid "Content (Markdown)" msgstr "內容 (Markdown格式)" -#: journal/forms.py:21 +#: journal/forms.py:21 journal/views/note.py:26 msgid "Post to Fediverse" msgstr "發佈到聯邦宇宙" @@ -1722,31 +1722,96 @@ msgstr "備註" #: journal/templates/action_open_post.html:14 #: journal/templates/action_open_post.html:16 #: journal/templates/collection_share.html:35 journal/templates/comment.html:35 -#: journal/templates/mark.html:93 journal/templates/note.html:32 -#: journal/templates/tag_edit.html:42 journal/templates/wrapped_share.html:43 -#: users/templates/users/data.html:47 users/templates/users/data.html:139 +#: journal/templates/mark.html:93 journal/templates/tag_edit.html:42 +#: journal/templates/wrapped_share.html:43 users/templates/users/data.html:47 +#: users/templates/users/data.html:139 #: users/templates/users/preferences.html:54 msgid "Public" msgstr "公開" #: journal/models/common.py:34 journal/templates/action_open_post.html:10 #: journal/templates/collection_share.html:46 journal/templates/comment.html:42 -#: journal/templates/mark.html:100 journal/templates/note.html:39 -#: journal/templates/wrapped_share.html:49 users/templates/users/data.html:55 -#: users/templates/users/data.html:147 +#: journal/templates/mark.html:100 journal/templates/wrapped_share.html:49 +#: users/templates/users/data.html:55 users/templates/users/data.html:147 #: users/templates/users/preferences.html:61 msgid "Followers Only" msgstr "僅關注者" #: journal/models/common.py:35 journal/templates/action_open_post.html:12 #: journal/templates/collection_share.html:57 journal/templates/comment.html:49 -#: journal/templates/mark.html:107 journal/templates/note.html:46 -#: journal/templates/wrapped_share.html:55 users/templates/users/data.html:63 -#: users/templates/users/data.html:155 +#: journal/templates/mark.html:107 journal/templates/wrapped_share.html:55 +#: users/templates/users/data.html:63 users/templates/users/data.html:155 #: users/templates/users/preferences.html:68 msgid "Mentioned Only" msgstr "自己和提到的人" +#: journal/models/note.py:27 +msgid "Page" +msgstr "頁碼" + +#: journal/models/note.py:28 +msgid "Chapter" +msgstr "章節" + +#: journal/models/note.py:31 +msgid "Part" +msgstr "分部" + +#: journal/models/note.py:32 +msgid "Episode" +msgstr "單集" + +#: journal/models/note.py:33 +msgid "Track" +msgstr "曲目" + +#: journal/models/note.py:34 +msgid "Cycle" +msgstr "周目" + +#: journal/models/note.py:35 +msgid "Timestamp" +msgstr "時間戳" + +#: journal/models/note.py:36 +msgid "Percentage" +msgstr "百分比" + +#: journal/models/note.py:53 +#, python-brace-format +msgid "Page {value}" +msgstr "第{value}頁" + +#: journal/models/note.py:54 +#, python-brace-format +msgid "Chapter {value}" +msgstr "第{value}章" + +#: journal/models/note.py:57 +#, python-brace-format +msgid "Part {value}" +msgstr "第{value}部" + +#: journal/models/note.py:58 +#, python-brace-format +msgid "Episode {value}" +msgstr "第{value}集" + +#: journal/models/note.py:59 +#, python-brace-format +msgid "Track {value}" +msgstr "第{value}首" + +#: journal/models/note.py:60 +#, python-brace-format +msgid "Cycle {value}" +msgstr "{value}周目" + +#: journal/models/renderers.py:94 mastodon/api.py:619 takahe/utils.py:540 +#, python-brace-format +msgid "regarding {item_title}, may contain spoiler or triggering content" +msgstr "關於 {item_title},可能包含劇透或敏感內容" + #: journal/models/shelf.py:24 msgid "WISHLIST" msgstr "" @@ -2323,7 +2388,6 @@ msgid "Tips: use >!text!< for spoilers; some instances may not be able to msgstr "提示: 善用 >!文字!< 標記可隱藏劇透; 超過360字可能無法分享到聯邦宇宙實例時間軸。" #: journal/templates/comment.html:62 journal/templates/mark.html:120 -#: journal/templates/note.html:59 msgid "Repost to timeline" msgstr "轉發到時間軸" @@ -2450,6 +2514,10 @@ msgstr "" msgid "Note" msgstr "筆記" +#: journal/templates/note.html:21 +msgid "Note with empty content will be deleted, sure to continue?" +msgstr "" + #: journal/templates/profile.html:55 msgid "calendar" msgstr "日曆" @@ -2458,7 +2526,7 @@ msgstr "日曆" msgid "annual summary" msgstr "年度小結" -#: journal/templates/profile.html:131 mastodon/api.py:747 +#: journal/templates/profile.html:131 mastodon/api.py:678 msgid "collection" msgstr "收藏單" @@ -2590,7 +2658,7 @@ msgstr "找不到條目,請使用本站條目網址。" msgid "Login required" msgstr "登錄後訪問" -#: journal/views/common.py:33 journal/views/mark.py:119 +#: journal/views/common.py:33 journal/views/mark.py:118 msgid "Data saved but unable to repost to Fediverse instance." msgstr "數據已保存但未能轉發到聯邦實例。" @@ -2602,15 +2670,35 @@ msgstr "正在重定向到你的聯邦實例以重新認證。" msgid "List not found." msgstr "列表未找到" -#: journal/views/mark.py:110 +#: journal/views/mark.py:109 msgid "Content too long for your Fediverse instance." msgstr "內容過長,超出了你的聯邦實例的限制。" -#: journal/views/mark.py:164 journal/views/mark.py:218 +#: journal/views/mark.py:163 journal/views/note.py:94 #: journal/views/review.py:30 msgid "Content not found" msgstr "內容未找到" +#: journal/views/note.py:44 +msgid "Progress (optional)" +msgstr "進度(選填)" + +#: journal/views/note.py:46 +msgid "Note Content" +msgstr "筆記內容" + +#: journal/views/note.py:48 +msgid "Content Warning (optional)" +msgstr "劇透或敏感內容提示(選填)" + +#: journal/views/note.py:62 +msgid "Progress Type (optional)" +msgstr "進度類型(選填)" + +#: journal/views/note.py:102 +msgid "Invalid form data" +msgstr "無效表單信息。" + #: journal/views/review.py:112 journal/views/review.py:126 #, python-brace-format msgid "Reviews by {0}" @@ -2649,16 +2737,11 @@ msgstr "標籤已更新" msgid "Summary posted to timeline." msgstr "總結已發佈到時間軸" -#: mastodon/api.py:600 takahe/utils.py:540 -#, python-brace-format -msgid "regarding {item_title}, may contain spoiler or triggering content" -msgstr "關於 {item_title},可能包含劇透或敏感內容" - -#: mastodon/api.py:752 +#: mastodon/api.py:683 msgid "shared my collection" msgstr "分享我的收藏單" -#: mastodon/api.py:755 +#: mastodon/api.py:686 #, python-brace-format msgid "shared {username}'s collection" msgstr "分享 {username} 的收藏單"