diff --git a/assets/css/base.css b/assets/css/base.css index 09370fa..53991bb 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -107,6 +107,9 @@ body { --color-poll-proportion-bar-bg: var(--light, var(--color-gray-250)) var(--dark, var(--color-dt-gray-300)); --color-poll-choice-bg: var(--light, var(--color-gray-150)) var(--dark, var(--color-dt-gray-500)); + --color-post-reveal-truncated-content-button: var(--light, var(--color-primary-500)) var(--dark, var(--color-primary-700)); + --color-post-reveal-truncated-content-button-hover: var(--light, var(--color-primary-400)) var(--dark, var(--color-primary-600)); + --color-post-footer: var(--light, var(--color-gray-500)) var(--dark, var(--color-dt-gray-250)); --color-post-footer-post-interaction: var(--light, var(--color-gray-500)) var(--dark, var(--color-dt-gray-225)); --color-post-tag-bg: var(--light, var(--color-gray-125)) var(--dark, var(--color-dt-gray-575)); diff --git a/assets/css/post-layout.css b/assets/css/post-layout.css index 394e693..5a59000 100644 --- a/assets/css/post-layout.css +++ b/assets/css/post-layout.css @@ -7,11 +7,39 @@ .post-body > .text-block:last-child, .post-body > .layout-row:last-child > .text-block:last-child, +.post-body > .layout-truncated:last-child > .text-block:last-child, .post-body > .link-block:last-child, -.post-body > .layout-row:last-child > .link-block:last-child { +.post-body > .layout-row:last-child > .link-block:last-child +.post-body > .layout-truncated:last-child > .text-block:last-child { margin-bottom: 0; } +.layout-row { + margin: 0; +} + +.layout-truncated > summary { + color: var(--color-post-reveal-truncated-content-button); + list-style: none; + font-weight: 650; + cursor: pointer; + text-decoration: underline; + display: inline-block; + + position: relative; + left: 50%; + transform: translateX(-50%); +} + +.layout-truncated > summary:hover { + color: var(--color-post-reveal-truncated-content-button-hover); +} + +.layout-truncated[open=""] > summary { + display: none; +} + + .heading1, .heading2, .quote { font-weight: 500; } diff --git a/assets/js/post.js b/assets/js/post.js index f096fb7..7e0f279 100644 --- a/assets/js/post.js +++ b/assets/js/post.js @@ -40,9 +40,9 @@ function fill_poll_results(poll_element, results) { } const answerIdChoiceElementArray = []; - const pollBody = poll_element.getElementsByClassName("poll-body")[0]; + const pollChoices = poll_element.getElementsByClassName("poll-choices")[0]; - for (let choiceElement of pollBody.children) { + for (let choiceElement of pollChoices.children) { answerIdChoiceElementArray.push([choiceElement.dataset.answerId, choiceElement]); } @@ -63,7 +63,7 @@ function fill_poll_results(poll_element, results) { voteProportionElement.classList.add("vote-proportion"); voteProportionElement["style"] = `width: ${((numericalVoteProportion) * 100).toFixed(3)}%;`; - const voteCountElement = document.createElement("span"); + const voteCountElement = document.createElement("p"); voteCountElement.classList.add("vote-count"); // A greater rounding precision is needed here diff --git a/locales/en_US/LC_MESSAGES/npf_renderer.po b/locales/en_US/LC_MESSAGES/npf_renderer.po new file mode 100644 index 0000000..712d89d --- /dev/null +++ b/locales/en_US/LC_MESSAGES/npf_renderer.po @@ -0,0 +1,84 @@ +# Copyright (C) 2023 Syeopite +# This file is distributed under the same license as the Priviblur project. +# syeopite , 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: Priviblur v0.3.0-dev\n" +"Report-Msgid-Bugs-To: https://github.com/syeopite/priviblur/issues\n" +"POT-Creation-Date: 2023-11-16 00:07-0800\n" +"PO-Revision-Date: 2024-06-29 04:09+0000\n" +"Last-Translator: syeopite \n" +"Language-Team: English \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.7-dev\n" + +msgid "asker_with_no_attribution" +msgstr "Anonymous" + +msgid "asker_and_ask_verb" +msgstr "{name} asked" + +msgid "unsupported_block_header" +msgstr "Unsupported NPF block" + +msgid "unsupported_block_description" +msgstr "Placeholder for the unsupported \"{block}\" type NPF block Please report me over at https://github.com/syeopite/npf-renderer" + +msgid "generic_image_alt_text" +msgstr "image" + +msgid "link_block_poster_alt_text" +msgstr "Preview image for \"{site}\"" + +msgid "link_block_fallback_embeds_are_disabled" +msgstr "Embeds are disabled" + +msgid "error_video_link_block_fallback_heading" +msgstr "Error: unable to render video block" + +msgid "video_link_block_fallback_description" +msgstr "Please click me to watch on the original site" + +msgid "error_link_block_fallback_native_video_player_non_tumblr_source" +msgstr "Error: non-tumblr source for video player" + +msgid "fallback_audio_block_thumbnail_alt_text" +msgstr "Album art" + +msgid "error_audio_link_block_fallback_heading" +msgstr "Error: unable to render audio block" + +msgid "audio_link_block_fallback_description" +msgstr "Please click me to listen on the original site" + +msgid "error_link_block_fallback_native_audio_player_non_tumblr_source" +msgstr "Error: non-tumblr source for audio player" + +msgid "poll_total_votes" +msgid_plural "poll_total_votes_plural" +msgstr[0] "{votes} vote" +msgstr[1] "{votes} votes" + +msgid "poll_remaining_time" +msgstr "{duration} remaining" + +msgid "poll_ended_on" +msgstr "Ended on: {ended_date}" + +msgid "post_attribution" +msgstr "From {0}" + +msgid "blog_attribution" +msgstr "Created by {0}" + +msgid "app_attribution" +msgstr "View on {0}" + +msgid "unsupported_attribution" +msgstr "Attributed via an unsupported (\"{0}\") attribution type. Please report this over at https://github.com/syeopite/npf-renderer" diff --git a/locales/en_US/LC_MESSAGES/priviblur.po b/locales/en_US/LC_MESSAGES/priviblur.po index 9afabbc..b53424e 100644 --- a/locales/en_US/LC_MESSAGES/priviblur.po +++ b/locales/en_US/LC_MESSAGES/priviblur.po @@ -296,4 +296,10 @@ msgid "post_note_viewer_view_replies_filter_sort_oldest" msgstr "Oldest first" msgid "post_note_viewer_view_replies_filter_sort_newest" -msgstr "Newest first" \ No newline at end of file +msgstr "Newest first" + +msgid "settings_expand_blogger_truncated_posts" +msgstr "Expand posts" + +msgid "settings_expand_blogger_truncated_posts_desc" +msgstr "Expands truncated posts automatically" diff --git a/src/config/user_preferences.py b/src/config/user_preferences.py index e7eb305..950bb01 100644 --- a/src/config/user_preferences.py +++ b/src/config/user_preferences.py @@ -9,3 +9,4 @@ class DefaultUserPreferences(NamedTuple): language: str = "en_US" theme: str = "auto" + expand_posts: bool = False diff --git a/src/helpers/ext_npf_renderer.py b/src/helpers/ext_npf_renderer.py index c3c7346..b894926 100644 --- a/src/helpers/ext_npf_renderer.py +++ b/src/helpers/ext_npf_renderer.py @@ -102,8 +102,30 @@ async def parse(self): class NPFFormatter(npf_renderer.format.Formatter): - def __init__(self, content, layout=None, blog_name=None, post_id=None, *, url_handler=None, forbid_external_iframes=False): - super().__init__(content, layout, url_handler=url_handler, forbid_external_iframes=forbid_external_iframes) + def __init__( + self, + content, + layout=None, + *, + blog_name=None, + post_id=None, + url_handler=None, + forbid_external_iframes=False, + request=None, + ): + initialization_arguments = { + "content": content, + "layout": layout, + "url_handler": url_handler, + "forbid_external_iframes": forbid_external_iframes + } + + if request: + # Asking to expand a post is the reverse of asking to truncate a post + initialization_arguments["truncate"] = not request.ctx.preferences.expand_posts + initialization_arguments["localizer"] = request.app.ctx.LANGUAGES[request.ctx.language].npf_renderer_localizer + + super().__init__(**initialization_arguments) # We store the blog and post ID as to be able to render a link to # fetch poll results for JS disabled users @@ -114,9 +136,9 @@ def _format_poll(self, block): poll_html = super()._format_poll(block) poll_html["data-poll-id"] = block.poll_id - poll_body = poll_html[1] + poll_choices = poll_html[1][0] for index, answer_id in enumerate(block.answers.keys()): - poll_body[index]["data-answer-id"] = answer_id + poll_choices[index]["data-answer-id"] = answer_id if (self.blog_name and self.post_id) and not block.votes: poll_footer = poll_html[2] @@ -171,7 +193,14 @@ def _add_alt_text_element(self, block, image_container): ) -async def format_npf(contents, layouts=None, blog_name=None, post_id=None,*, poll_callback=None): +async def format_npf( + contents, + layouts=None, + blog_name=None, + post_id=None,*, + poll_callback=None, + request=None +): """Wrapper around npf_renderer.format_npf for extra functionalities - Replaces internal Parser and Formatter with the modified variants above @@ -183,6 +212,8 @@ async def format_npf(contents, layouts=None, blog_name=None, post_id=None,*, pol Name of the blog the post comes from. This is used to render links to the parent post post_id: Unique ID of the post. This is used to render links to the parent post + request: + Sanic request object. Used to check user preferences """ try: contents = await NPFParser(contents, poll_callback=poll_callback).parse() @@ -197,6 +228,7 @@ async def format_npf(contents, layouts=None, blog_name=None, post_id=None,*, pol post_id=post_id, url_handler=url_handler, forbid_external_iframes=True, + request=request ).format() except npf_renderer.exceptions.RenderErrorDisclaimerError as e: diff --git a/src/helpers/i18n.py b/src/helpers/i18n.py deleted file mode 100644 index 363c97b..0000000 --- a/src/helpers/i18n.py +++ /dev/null @@ -1,73 +0,0 @@ -import sys -import gettext -import typing - -import sanic - -from .i18n_data import LOCALE_DATA - - -class Language: - """Stores metadata about supported translations""" - def __init__(self, locale, gettext_instance,) -> None: - self.locale = locale - self.instance = gettext_instance - - self.name, self.translation_percentage = LOCALE_DATA[locale] - -SUPPORTED_LANGUAGES = [ - "en_US", "cs_CZ", "fr", "ja", "uk", "zh_Hans", "zh_Hant", "es" -] - -SUPPORTED_LANGUAGES.sort() - - -def initialize_locales() -> typing.Mapping[str, Language]: - """Initializes locales into GNUTranslations instances""" - try: - # Initialize english locale first so that we may use it as a fallback - - english_instance = gettext.translation("priviblur", localedir="locales", languages=("en_US",)) - - languages = { - "en_US": Language("en_US", english_instance) - } - - for locale in SUPPORTED_LANGUAGES: - if locale == "en_US": - continue - - instance = gettext.translation("priviblur", localedir="locales", languages=(locale,)) - instance.add_fallback(english_instance) - - languages[locale] = Language(locale, instance) - except FileNotFoundError as e: - print( - 'Error: Unable to find locale files. ' - 'Did you forget to compile them?' - ) - - sys.exit() - except Exception as e: - raise e - - return languages - - -def translate(language : str, id : str, number : int | float | None = None, - substitution : str | dict | None = None) -> str: - app = sanic.Sanic.get_app("Priviblur") - - gettext_instance = app.ctx.LANGUAGES[language].instance - - if number is not None: - translated = gettext_instance.ngettext(id, f"{id}_plural", number) - else: - translated = gettext_instance.gettext(id) - - if isinstance(substitution, str): - translated = translated.format(substitution) - elif isinstance(substitution, dict): - translated = translated.format(**substitution) - - return translated diff --git a/src/i18n/__init__.py b/src/i18n/__init__.py new file mode 100644 index 0000000..a18e3b5 --- /dev/null +++ b/src/i18n/__init__.py @@ -0,0 +1 @@ +from .i18n import SUPPORTED_LANGUAGES, initialize_locales, translate \ No newline at end of file diff --git a/src/i18n/i18n.py b/src/i18n/i18n.py new file mode 100644 index 0000000..faafd30 --- /dev/null +++ b/src/i18n/i18n.py @@ -0,0 +1,125 @@ +import sys +import gettext +import typing +import functools + +import sanic +import babel +import babel.dates +import npf_renderer + +from .i18n_data import LOCALE_DATA + + +class NPFRendererGettextFallback(gettext.NullTranslations): + def gettext(self, message): + return npf_renderer.DEFAULT_LOCALIZATION[message] + + def ngettext(self, msgid1: str, msgid2: str, n: int) -> str: + return npf_renderer.DEFAULT_LOCALIZATION[msgid1] + + +class NPFRendererLocalizer: + def __init__(self, language, locale): + self.language = language + self.locale = locale + + self.format_functions = { + "format_duration_func": functools.partial(babel.dates.format_timedelta, threshold=1.1, locale=language), + "format_datetime_func": functools.partial(babel.dates.format_datetime, format=f"short", locale=language), + } + + def __getitem__(self, key : str): + # Starts with format_ + if key[:7] == "format_": + return self.format_functions[key] + # Starts with plural_ + elif key[:7] == "plural_": + return lambda number : translate(self.language, key[7:], number, priviblur_translations=False) + + translate("en_US", "poll_remaining_time", priviblur_translations=False) + + return translate(self.language, key, priviblur_translations=False) + + +class Language: + """Stores metadata about supported translations""" + def __init__(self, locale, priviblur_gettext, npf_renderer_gettext) -> None: + self.locale = locale + + self.babel_locale = babel.Locale.parse(locale) + + self.priviblur_translations = priviblur_gettext + + self.npf_renderer_translations = npf_renderer_gettext + self.npf_renderer_localizer = NPFRendererLocalizer(locale, self.babel_locale) + + self.name, self.translation_percentage = LOCALE_DATA[locale] + +SUPPORTED_LANGUAGES = [ + "en_US", "cs_CZ", "fr", "ja", "uk", "zh_Hans", "zh_Hant", "es" +] + +SUPPORTED_LANGUAGES.sort() + + +def initialize_locales() -> typing.Mapping[str, Language]: + """Initializes locales into GNUTranslations instances""" + try: + # Initialize english locale first so that we may use it as a fallback + + priviblur_english_instance = gettext.translation("priviblur", localedir="locales", languages=("en_US",)) + + npf_renderer_english_instance = gettext.translation("npf_renderer", localedir="locales", languages=("en_US",)) + npf_renderer_english_instance.add_fallback(NPFRendererGettextFallback()) + + languages = { + "en_US": Language("en_US", priviblur_english_instance, npf_renderer_english_instance) + } + + for locale in SUPPORTED_LANGUAGES: + if locale == "en_US": + continue + + instance = gettext.translation("priviblur", localedir="locales", languages=(locale,)) + instance.add_fallback(priviblur_english_instance) + + try: + npf_renderer_instance = gettext.translation("npf_renderer", localedir="locales", languages=(locale,)) + except FileNotFoundError: + npf_renderer_instance = npf_renderer_english_instance + + languages[locale] = Language(locale, instance, npf_renderer_instance) + except FileNotFoundError as e: + print( + 'Error: Unable to find locale files. ' + 'Did you forget to compile them?' + ) + + sys.exit() + except Exception as e: + raise e + + return languages + + +def translate(language : str, id : str, number : int | float | None = None, + substitution : str | dict | None = None, priviblur_translations : bool = True) -> str: + app = sanic.Sanic.get_app("Priviblur") + + if priviblur_translations: + gettext_instance = app.ctx.LANGUAGES[language].priviblur_translations + else: + gettext_instance = app.ctx.LANGUAGES[language].npf_renderer_translations + + if number is not None: + translated = gettext_instance.ngettext(id, f"{id}_plural", number) + else: + translated = gettext_instance.gettext(id) + + if isinstance(substitution, str): + translated = translated.format(substitution) + elif isinstance(substitution, dict): + translated = translated.format(**substitution) + + return translated diff --git a/src/helpers/i18n_data.py b/src/i18n/i18n_data.py similarity index 100% rename from src/helpers/i18n_data.py rename to src/i18n/i18n_data.py diff --git a/src/preferences.py b/src/preferences.py index 2411352..a449b09 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -1,7 +1,7 @@ import dataclasses import urllib.parse -from .helpers.i18n import SUPPORTED_LANGUAGES +from .i18n import SUPPORTED_LANGUAGES VERSION = 1 @@ -11,6 +11,8 @@ class UserPreferences: language: str theme: str + expand_posts: bool + # Tracks major revisions of the settings cookie # Only bump in case of breaking changes. version: int = 1 diff --git a/src/server.py b/src/server.py index e970c82..d58cbd4 100644 --- a/src/server.py +++ b/src/server.py @@ -12,10 +12,10 @@ import redis.asyncio from npf_renderer import VERSION as NPF_RENDERER_VERSION -from . import routes, priviblur_extractor, preferences +from . import routes, priviblur_extractor, preferences, i18n from .exceptions import error_handlers from .config import load_config -from .helpers import setup_logging, helpers, i18n, ext_npf_renderer +from .helpers import setup_logging, helpers, ext_npf_renderer from .version import VERSION, CURRENT_COMMIT diff --git a/src/templates/components/blog_header.jinja b/src/templates/components/blog_header.jinja index 4418179..0d177d3 100644 --- a/src/templates/components/blog_header.jinja +++ b/src/templates/components/blog_header.jinja @@ -1,5 +1,5 @@ {% if blog.blog_info.description_npf -%} - {% set contains_errors, content_tag = format_npf(blog.blog_info.description_npf) -%} + {% set contains_errors, content_tag = format_npf(blog.blog_info.description_npf, request=request) -%} {% else -%} {% set content_tag = "" %} {% endif -%} diff --git a/src/templates/post/components/body.jinja b/src/templates/post/components/body.jinja index ca135d0..6cc376b 100644 --- a/src/templates/post/components/body.jinja +++ b/src/templates/post/components/body.jinja @@ -4,6 +4,7 @@ element.content, element.layout, poll_callback=create_poll_callback(request.app.ctx, element.blog.name, element.id), + request=request, ) -%} {%- else -%} @@ -12,6 +13,7 @@ element.layout, element.blog.name, element.id, + request=request, ) -%} {% endif %} @@ -38,6 +40,7 @@ trail_element.content, trail_element.layout, poll_callback=create_poll_callback(request.app.ctx, element.blog.name, element.id), + request=request, ) -%} @@ -47,6 +50,7 @@ trail_element.layout, element.blog.name, element.id, + request=request, ) -%} {% endif %} diff --git a/src/templates/post/notes/note/reply.jinja b/src/templates/post/notes/note/reply.jinja index 5eed58a..6d18a6b 100644 --- a/src/templates/post/notes/note/reply.jinja +++ b/src/templates/post/notes/note/reply.jinja @@ -30,6 +30,7 @@ note.layout, note.blog.name, note.id, + request=request, ) -%} diff --git a/src/templates/settings.jinja b/src/templates/settings.jinja index 31f38e0..d435a2b 100644 --- a/src/templates/settings.jinja +++ b/src/templates/settings.jinja @@ -58,6 +58,16 @@ {%- endfor %} +
+
+
+ +

{{translate(request.ctx.language, "settings_expand_blogger_truncated_posts_desc")}}

+
+ + + +