From 810a54076e054829281f7cc468c7cf48a0c7518e Mon Sep 17 00:00:00 2001 From: deing Date: Fri, 30 Aug 2019 17:41:37 +0200 Subject: [PATCH] Add a Theme Preview page in Settings --- tildes/scss/modules/_settings.scss | 16 ++ tildes/scss/themes/_atom_one_dark.scss | 3 + tildes/scss/themes/_black.scss | 3 + tildes/scss/themes/_default.scss | 3 + tildes/scss/themes/_dracula.scss | 3 + tildes/scss/themes/_gruvbox.scss | 6 + tildes/scss/themes/_solarized.scss | 6 + tildes/scss/themes/_theme_base.scss | 10 ++ tildes/scss/themes/_zenburn.scss | 3 + tildes/tildes/routes.py | 4 + tildes/tildes/templates/settings.jinja2 | 2 + .../templates/settings_theme_previews.jinja2 | 77 ++++++++++ tildes/tildes/views/settings.py | 137 ++++++++++++++++++ 13 files changed, 273 insertions(+) create mode 100644 tildes/tildes/templates/settings_theme_previews.jinja2 diff --git a/tildes/scss/modules/_settings.scss b/tildes/scss/modules/_settings.scss index 9b1c874..8d8995f 100644 --- a/tildes/scss/modules/_settings.scss +++ b/tildes/scss/modules/_settings.scss @@ -28,3 +28,19 @@ border-color: inherit; } } + +.theme-preview-blocks { + display: flex; + max-width: 40rem; + flex-wrap: wrap; + // The colors are assigned by a mixin from _theme_base.scss called in each theme file + span { + display: block; + flex-grow: 1; + min-width: 6rem; + text-align: center; + padding: 1rem; + font-weight: bold; + margin: .3rem; + } +} \ No newline at end of file diff --git a/tildes/scss/themes/_atom_one_dark.scss b/tildes/scss/themes/_atom_one_dark.scss index bc29d51..2f137f7 100644 --- a/tildes/scss/themes/_atom_one_dark.scss +++ b/tildes/scss/themes/_atom_one_dark.scss @@ -47,3 +47,6 @@ $theme-atom-one-dark: ( body.theme-atom-one-dark { @include use-theme($theme-atom-one-dark); } +body { + @include theme-preview-block($theme-atom-one-dark, "atom-one-dark"); +} \ No newline at end of file diff --git a/tildes/scss/themes/_black.scss b/tildes/scss/themes/_black.scss index f1630ab..52c20ec 100644 --- a/tildes/scss/themes/_black.scss +++ b/tildes/scss/themes/_black.scss @@ -26,3 +26,6 @@ $theme-black: ( body.theme-black { @include use-theme($theme-black); } +body { + @include theme-preview-block($theme-black, "black"); +} \ No newline at end of file diff --git a/tildes/scss/themes/_default.scss b/tildes/scss/themes/_default.scss index 368c81f..ad8e9cf 100644 --- a/tildes/scss/themes/_default.scss +++ b/tildes/scss/themes/_default.scss @@ -31,3 +31,6 @@ $default-theme: ( body { @include use-theme($default-theme); } +body { + @include theme-preview-block($default-theme, "white"); +} \ No newline at end of file diff --git a/tildes/scss/themes/_dracula.scss b/tildes/scss/themes/_dracula.scss index 8e8cc2a..404195e 100644 --- a/tildes/scss/themes/_dracula.scss +++ b/tildes/scss/themes/_dracula.scss @@ -49,3 +49,6 @@ $theme-dracula: ( body.theme-dracula { @include use-theme($theme-dracula); } +body { + @include theme-preview-block($theme-dracula, "dracula"); +} diff --git a/tildes/scss/themes/_gruvbox.scss b/tildes/scss/themes/_gruvbox.scss index bb146d7..c0c28db 100644 --- a/tildes/scss/themes/_gruvbox.scss +++ b/tildes/scss/themes/_gruvbox.scss @@ -110,6 +110,9 @@ $gruvbox-dark: ( body.theme-gruvbox-dark { @include use-theme(map-merge($gruvbox-base, $gruvbox-dark)); } +body { + @include theme-preview-block(map-merge($gruvbox-base, $gruvbox-dark), "gruvbox-dark"); +} // Light theme definition $gruvbox-light: ( @@ -130,3 +133,6 @@ $gruvbox-light: ( body.theme-gruvbox-light { @include use-theme(map-merge($gruvbox-base, $gruvbox-light)); } +body { + @include theme-preview-block(map-merge($gruvbox-base, $gruvbox-light), "gruvbox-light"); +} diff --git a/tildes/scss/themes/_solarized.scss b/tildes/scss/themes/_solarized.scss index 152c685..d9fdfd7 100644 --- a/tildes/scss/themes/_solarized.scss +++ b/tildes/scss/themes/_solarized.scss @@ -67,6 +67,9 @@ $solarized-dark: ( body.theme-solarized-dark { @include use-theme(map-merge($solarized-base, $solarized-dark)); } +body { + @include theme-preview-block(map-merge($solarized-base, $solarized-dark), "solarized-dark"); +} // Light theme definition $solarized-light: ( @@ -84,3 +87,6 @@ $solarized-light: ( body.theme-solarized-light { @include use-theme(map-merge($solarized-base, $solarized-light)); } +body { + @include theme-preview-block(map-merge($solarized-base, $solarized-light), "solarized-light"); +} diff --git a/tildes/scss/themes/_theme_base.scss b/tildes/scss/themes/_theme_base.scss index 83286fe..6511714 100644 --- a/tildes/scss/themes/_theme_base.scss +++ b/tildes/scss/themes/_theme_base.scss @@ -917,3 +917,13 @@ "topic-tag-spoiler": $topic-tag-spoiler, )); } + +@mixin theme-preview-block($theme, $name) { + .theme-preview-blocks { + .theme-preview-block-#{$name} { + background-color: map-get($theme, "background-primary"); + color: map-get($theme, "foreground-primary"); + border: 1px solid map-get($theme, "background-secondary"); + } + } +} \ No newline at end of file diff --git a/tildes/scss/themes/_zenburn.scss b/tildes/scss/themes/_zenburn.scss index 3448205..75b813a 100644 --- a/tildes/scss/themes/_zenburn.scss +++ b/tildes/scss/themes/_zenburn.scss @@ -39,3 +39,6 @@ $theme-zenburn: ( body.theme-zenburn { @include use-theme($theme-zenburn); } +body { + @include theme-preview-block($theme-zenburn, "zenburn"); +} \ No newline at end of file diff --git a/tildes/tildes/routes.py b/tildes/tildes/routes.py index 766b115..e9111a5 100644 --- a/tildes/tildes/routes.py +++ b/tildes/tildes/routes.py @@ -18,6 +18,7 @@ from tildes.resources.user import user_by_username def includeme(config: Configurator) -> None: """Set up application routes.""" + # pylint: disable=too-many-statements config.add_route("home", "/") config.add_route("search", "/search") @@ -99,6 +100,9 @@ def includeme(config: Configurator) -> None: config.add_route( "settings_password_change", "/password_change", factory=LoggedInFactory ) + config.add_route( + "settings_theme_previews", "/theme_previews", factory=LoggedInFactory + ) config.add_route("bookmarks", "/bookmarks", factory=LoggedInFactory) diff --git a/tildes/tildes/templates/settings.jinja2 b/tildes/tildes/templates/settings.jinja2 index bfd9d58..7ed99f4 100644 --- a/tildes/tildes/templates/settings.jinja2 +++ b/tildes/tildes/templates/settings.jinja2 @@ -34,6 +34,8 @@ {% endfor %} + Preview themes + diff --git a/tildes/tildes/templates/settings_theme_previews.jinja2 b/tildes/tildes/templates/settings_theme_previews.jinja2 new file mode 100644 index 0000000..963f5b2 --- /dev/null +++ b/tildes/tildes/templates/settings_theme_previews.jinja2 @@ -0,0 +1,77 @@ +{# Copyright (c) 2018 Tildes contributors #} +{# SPDX-License-Identifier: AGPL-3.0-or-later #} + +{% from 'macros/comments.jinja2' import render_comment_tree, comment_label_options_template, comment_reply_template with context %} +{% from 'macros/topics.jinja2' import render_topic_for_listing with context %} + +{% extends 'base_settings.jinja2' %} + +{% block main_classes %}{% endblock %} + +{% block title %}Theme previews{% endblock %} + +{% block main_heading %}Theme previews{% endblock %} + +{% block settings %} + +
+ + + Return to Settings + + +
+ +
+

Quick overview

+
+ {% for theme, description in theme_options.items() %} + {# The theme dict includes various (default) texts in the descriptions, cut them off #} + {# Also, replace all spaces with NBSPs so the theme names don't get linewrapped #} + {{ description.split(" (")[0]|replace(" ", "\u00a0") }} + {% endfor %} +
+
+ +
+

Topic listings

+
    + {% for fake_topic in fake_topics %} +
  1. + {{ render_topic_for_listing(fake_topic, show_group=true) }} +
  2. + {% endfor %} +
+
+
+

Comments

+
    + {{ render_comment_tree(fake_comment_tree, mark_newer_than=last_visit, is_individual_comment=False) }} +
+
+{% endblock %} + +{% block templates %} + {% if request.user %} + {{ comment_reply_template() }} + {{ comment_label_options_template(comment_label_options) }} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/tildes/tildes/views/settings.py b/tildes/tildes/views/settings.py index 29ef6b3..2c86ac9 100644 --- a/tildes/tildes/views/settings.py +++ b/tildes/tildes/views/settings.py @@ -4,6 +4,7 @@ """Views related to user settings.""" from io import BytesIO +from typing import List, Optional import pyotp import qrcode @@ -13,7 +14,13 @@ from pyramid.response import Response from pyramid.view import view_config from webargs.pyramidparser import use_kwargs +from tildes.enums import CommentLabelOption, CommentTreeSortOption from tildes.lib.string import separate_string +from tildes.lib.datetime import utc_from_timestamp, utc_now +from tildes.models.comment import Comment, CommentLabel, CommentTree +from tildes.models.group import Group +from tildes.models.topic import Topic +from tildes.models.user import User from tildes.schemas.user import ( BIO_MAX_LENGTH, EMAIL_ADDRESS_NOTE_MAX_LENGTH, @@ -27,6 +34,11 @@ PASSWORD_FIELD = UserSchema(only=("password",)).fields["password"] @view_config(route_name="settings", renderer="settings.jinja2") def get_settings(request: Request) -> dict: """Generate the user settings page.""" + return generate_theme_chooser_dict(request) + + +def generate_theme_chooser_dict(request: Request) -> dict: + """Generate the partial response dict necessary for the settings theme selector.""" site_default_theme = "white" user_default_theme = request.user.theme_default or site_default_theme @@ -144,3 +156,128 @@ def post_settings_password_change( request.user.change_password(old_password, new_password) return Response("Your password has been updated") + + +@view_config( + route_name="settings_theme_previews", renderer="settings_theme_previews.jinja2" +) +def get_settings_theme_previews(request: Request) -> dict: + """Generate the theme preview page. + + On the site, the following data must not point to real data users could + inadvertently affect with the demo widgets: + - The user @Tildes + - The group ~groupname + - Topic ID 42_000_000_000 + - Comment IDs 42_000_000_000 through 42_000_000_003 + """ + + fake_old_timestamp = utc_from_timestamp(int(utc_now().timestamp() - 60 * 60 * 24)) + fake_last_visit_timestamp = utc_from_timestamp( + int(utc_now().timestamp() - 60 * 60 * 12) + ) + + fake_user = User("Tildes", "a_very_safe_password") + fake_user.user_id = 0 + fake_user_not_op = User("Tildes", "another_very_safe_password") + fake_user_not_op.user_id = -1 + fake_group = Group("groupname") + fake_group.is_user_treated_as_topic_source = False + + fake_topics: List[Topic] = [ + Topic.create_link_topic( + fake_group, fake_user, "Example Link Topic", "https://tildes.net/" + ), + Topic.create_text_topic( + fake_group, fake_user, "Example Text Topic", "empty string" + ), + ] + + for fake_topic in fake_topics: + fake_topic.topic_id = 42_000_000_000 + fake_topic.tags = ["a tag", "another tag"] + fake_topic.created_time = utc_now() + fake_topic.group = fake_group + fake_topic.num_comments = 0 + fake_topic.num_votes = 0 + fake_topic.content_metadata = { + "excerpt": """Lorem ipsum dolor sit amet, + consectetur adipiscing elit. Nunc auctor purus at diam tempor, + id viverra nunc vulputate.""", + "word_count": 42, + } + + def make_comment( + markdown: str, comment_id: int, parent_id: Optional[int], is_op: bool + ) -> Comment: + """Create a fake comment with enough data to make the template render fine.""" + fake_comment = Comment(fake_topics[0], fake_user_not_op, markdown) + fake_comment.comment_id = comment_id + if parent_id: + fake_comment.parent_comment_id = parent_id + fake_comment.created_time = fake_old_timestamp + fake_comment.num_votes = 0 + if is_op: + fake_comment.user = fake_user + return fake_comment + + fake_comments: List[Comment] = [] + + fake_comments.append( + make_comment( + """This is a regular comment, written by yourself. \ + It has **formatting** and a [link](https://tildes.net).""", + 42_000_000_000, + None, + False, + ) + ) + fake_comments[-1].user = request.user + fake_comments.append( + make_comment( + """This is a reply written by the topic's OP. \ + It's new and has the *Mark New Comments* stripe on its left, \ + even if you didn't enable that feature.""", + 42_000_000_001, + 42_000_000_000, + True, + ) + ) + fake_comments[-1].created_time = utc_now() + fake_comments.append( + make_comment( + """This reply is Exemplary. It also has a blockquote:\ + \n> Hello World!""", + 42_000_000_002, + 42_000_000_000, + False, + ) + ) + fake_comments[-1].labels.append( + CommentLabel(fake_comments[-1], fake_user, CommentLabelOption.EXEMPLARY, 1.0) + ) + fake_comments.append( + make_comment( + """This is a regular reply with a code block in it:\ + \n```js\ + \nfunction foo() {\ + \n ['1', '2', '3'].map(parseInt);\ + \n}\ + \n```""", + 42_000_000_003, + 42_000_000_000, + False, + ) + ) + + fake_tree = CommentTree( + fake_comments, CommentTreeSortOption.RELEVANCE, request.user + ) + + return { + **generate_theme_chooser_dict(request), + "fake_topics": fake_topics, + "fake_comment_tree": fake_tree, + "comment_label_options": [label for label in CommentLabelOption], + "last_visit": fake_last_visit_timestamp, + }