diff --git a/tildes/scss/_base.scss b/tildes/scss/_base.scss index e0d73d5..b827ef8 100644 --- a/tildes/scss/_base.scss +++ b/tildes/scss/_base.scss @@ -35,6 +35,10 @@ body { @include font-shrink-on-mobile(0.8rem); } +button { + cursor: pointer; +} + code { display: inline-block; font-size: inherit; diff --git a/tildes/scss/modules/_settings.scss b/tildes/scss/modules/_settings.scss index 8d8995f..9b1c874 100644 --- a/tildes/scss/modules/_settings.scss +++ b/tildes/scss/modules/_settings.scss @@ -28,19 +28,3 @@ 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/modules/_theme-preview.scss b/tildes/scss/modules/_theme-preview.scss new file mode 100644 index 0000000..4e95502 --- /dev/null +++ b/tildes/scss/modules/_theme-preview.scss @@ -0,0 +1,28 @@ +// Copyright (c) 2019 Tildes contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +.theme-preview-blocks { + display: flex; + flex-wrap: wrap; + max-width: $paragraph-max-width; +} + +.theme-preview-block { + min-width: 6rem; + margin: 0.4rem; + padding: 1rem; + + text-align: center; + font-weight: bold; + white-space: nowrap; +} + +.theme-preview-fake-posts { + // Disables all click events (and hover) on the fake posts so links/buttons don't work + pointer-events: none; + + // Set a max width on the fake topics so the vote button isn't way off to the right + .topic { + max-width: $paragraph-max-width; + } +} diff --git a/tildes/scss/styles.scss b/tildes/scss/styles.scss index 2ed6ab9..4076930 100644 --- a/tildes/scss/styles.scss +++ b/tildes/scss/styles.scss @@ -35,6 +35,7 @@ @import "modules/static-site"; @import "modules/tab"; @import "modules/text"; +@import "modules/theme-preview"; @import "modules/time"; @import "modules/toast"; @import "modules/topic"; diff --git a/tildes/scss/themes/_atom_one_dark.scss b/tildes/scss/themes/_atom_one_dark.scss index 2f137f7..8e1cb90 100644 --- a/tildes/scss/themes/_atom_one_dark.scss +++ b/tildes/scss/themes/_atom_one_dark.scss @@ -47,6 +47,5 @@ $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 + +@include theme-preview-block($theme-atom-one-dark, "atom-one-dark"); diff --git a/tildes/scss/themes/_black.scss b/tildes/scss/themes/_black.scss index 52c20ec..7f73cee 100644 --- a/tildes/scss/themes/_black.scss +++ b/tildes/scss/themes/_black.scss @@ -26,6 +26,5 @@ $theme-black: ( body.theme-black { @include use-theme($theme-black); } -body { - @include theme-preview-block($theme-black, "black"); -} \ No newline at end of file + +@include theme-preview-block($theme-black, "black"); diff --git a/tildes/scss/themes/_default.scss b/tildes/scss/themes/_default.scss index ad8e9cf..0e10e97 100644 --- a/tildes/scss/themes/_default.scss +++ b/tildes/scss/themes/_default.scss @@ -31,6 +31,5 @@ $default-theme: ( body { @include use-theme($default-theme); } -body { - @include theme-preview-block($default-theme, "white"); -} \ No newline at end of file + +@include theme-preview-block($default-theme, "white"); diff --git a/tildes/scss/themes/_dracula.scss b/tildes/scss/themes/_dracula.scss index 404195e..c5599df 100644 --- a/tildes/scss/themes/_dracula.scss +++ b/tildes/scss/themes/_dracula.scss @@ -49,6 +49,5 @@ $theme-dracula: ( body.theme-dracula { @include use-theme($theme-dracula); } -body { - @include theme-preview-block($theme-dracula, "dracula"); -} + +@include theme-preview-block($theme-dracula, "dracula"); diff --git a/tildes/scss/themes/_gruvbox.scss b/tildes/scss/themes/_gruvbox.scss index c0c28db..90f04f6 100644 --- a/tildes/scss/themes/_gruvbox.scss +++ b/tildes/scss/themes/_gruvbox.scss @@ -110,9 +110,8 @@ $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"); -} + +@include theme-preview-block(map-merge($gruvbox-base, $gruvbox-dark), "gruvbox-dark"); // Light theme definition $gruvbox-light: ( @@ -133,6 +132,5 @@ $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"); -} + +@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 d9fdfd7..0e67221 100644 --- a/tildes/scss/themes/_solarized.scss +++ b/tildes/scss/themes/_solarized.scss @@ -67,9 +67,11 @@ $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"); -} + +@include theme-preview-block( + map-merge($solarized-base, $solarized-dark), + "solarized-dark" +); // Light theme definition $solarized-light: ( @@ -87,6 +89,8 @@ $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"); -} + +@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 6511714..7298881 100644 --- a/tildes/scss/themes/_theme_base.scss +++ b/tildes/scss/themes/_theme_base.scss @@ -919,11 +919,9 @@ } @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"); - } + .theme-preview-block-#{$name} { + background-color: map-get($theme, "background-primary"); + color: map-get($theme, "foreground-primary"); + border: 1px solid; } -} \ No newline at end of file +} diff --git a/tildes/scss/themes/_zenburn.scss b/tildes/scss/themes/_zenburn.scss index 75b813a..b284f8d 100644 --- a/tildes/scss/themes/_zenburn.scss +++ b/tildes/scss/themes/_zenburn.scss @@ -39,6 +39,5 @@ $theme-zenburn: ( body.theme-zenburn { @include use-theme($theme-zenburn); } -body { - @include theme-preview-block($theme-zenburn, "zenburn"); -} \ No newline at end of file + +@include theme-preview-block($theme-zenburn, "zenburn"); diff --git a/tildes/static/js/behaviors/theme-preview.js b/tildes/static/js/behaviors/theme-preview.js new file mode 100644 index 0000000..09fd822 --- /dev/null +++ b/tildes/static/js/behaviors/theme-preview.js @@ -0,0 +1,10 @@ +// Copyright (c) 2019 Tildes contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +$.onmount("[data-js-theme-preview]", function() { + $(this).click(function() { + var newTheme = $(this).attr("data-js-theme-preview"); + + Tildes.changeTheme(newTheme); + }); +}); diff --git a/tildes/static/js/behaviors/theme-selector.js b/tildes/static/js/behaviors/theme-selector.js index 0d96d63..912cf01 100644 --- a/tildes/static/js/behaviors/theme-selector.js +++ b/tildes/static/js/behaviors/theme-selector.js @@ -25,18 +25,7 @@ $.onmount("[data-js-theme-selector]", function() { "path=/;max-age=315360000;secure;domain=" + document.location.hostname; - // remove any theme classes currently on the body - var $body = $("body").first(); - var bodyClasses = $body[0].className.split(" "); - for (var i = 0; i < bodyClasses.length; i++) { - var cls = bodyClasses[i]; - if (cls.indexOf("theme-") === 0) { - $body.removeClass(cls); - } - } - - // add the class for the new theme to the body - $body.addClass("theme-" + new_theme); + Tildes.changeTheme(new_theme); // set visibility of 'Set as account default' button if (selected_text.indexOf("account default") === -1) { diff --git a/tildes/static/js/scripts.js b/tildes/static/js/scripts.js index d11040a..9678546 100644 --- a/tildes/static/js/scripts.js +++ b/tildes/static/js/scripts.js @@ -89,3 +89,18 @@ $(function() { if (!window.Tildes) { window.Tildes = {}; } + +Tildes.changeTheme = function(newThemeName) { + // remove any theme classes currently on the body + var $body = $("body").first(); + var bodyClasses = $body[0].className.split(" "); + for (var i = 0; i < bodyClasses.length; i++) { + var cls = bodyClasses[i]; + if (cls.indexOf("theme-") === 0) { + $body.removeClass(cls); + } + } + + // add the class for the new theme to the body + $body.addClass("theme-" + newThemeName); +}; diff --git a/tildes/tildes/routes.py b/tildes/tildes/routes.py index e9111a5..55731dc 100644 --- a/tildes/tildes/routes.py +++ b/tildes/tildes/routes.py @@ -18,7 +18,6 @@ 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") diff --git a/tildes/tildes/templates/settings.jinja2 b/tildes/tildes/templates/settings.jinja2 index 7ed99f4..25ffb27 100644 --- a/tildes/tildes/templates/settings.jinja2 +++ b/tildes/tildes/templates/settings.jinja2 @@ -34,12 +34,12 @@ {% endfor %} - Preview themes - + + View theme previews
  • diff --git a/tildes/tildes/templates/settings_theme_previews.jinja2 b/tildes/tildes/templates/settings_theme_previews.jinja2 index 963f5b2..8c2de89 100644 --- a/tildes/tildes/templates/settings_theme_previews.jinja2 +++ b/tildes/tildes/templates/settings_theme_previews.jinja2 @@ -1,7 +1,7 @@ -{# Copyright (c) 2018 Tildes contributors #} +{# Copyright (c) 2019 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/comments.jinja2' import render_comment_tree with context %} {% from 'macros/topics.jinja2' import render_topic_for_listing with context %} {% extends 'base_settings.jinja2' %} @@ -13,47 +13,22 @@ {% block main_heading %}Theme previews{% endblock %} {% block settings %} - -
    - - - Return to Settings - - -
    -
    -

    Quick overview

    +

    Theme options

    +

    Click a theme to preview it on this page. This will not change your current theme setting. If you want to continue using a theme, select it on the main Settings page.

    - {% 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") }} + {% for class_name, name in theme_options.items() %} + {% endfor %}

    Topic listings

    -
      +
        {% for fake_topic in fake_topics %}
      1. {{ render_topic_for_listing(fake_topic, show_group=true) }} @@ -61,17 +36,11 @@ {% endfor %}
    +

    Comments

    -
      - {{ render_comment_tree(fake_comment_tree, mark_newer_than=last_visit, is_individual_comment=False) }} +
        + {{ render_comment_tree(fake_comment_tree, mark_newer_than=last_visit) }}
    {% 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 2c86ac9..9324b38 100644 --- a/tildes/tildes/views/settings.py +++ b/tildes/tildes/views/settings.py @@ -3,8 +3,9 @@ """Views related to user settings.""" +from datetime import timedelta from io import BytesIO -from typing import List, Optional +import sys import pyotp import qrcode @@ -12,11 +13,12 @@ from pyramid.httpexceptions import HTTPForbidden, HTTPUnprocessableEntity from pyramid.request import Request from pyramid.response import Response from pyramid.view import view_config +from sqlalchemy import func from webargs.pyramidparser import use_kwargs from tildes.enums import CommentLabelOption, CommentTreeSortOption +from tildes.lib.datetime import utc_now 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 @@ -30,31 +32,29 @@ from tildes.schemas.user import ( PASSWORD_FIELD = UserSchema(only=("password",)).fields["password"] +THEME_OPTIONS = { + "white": "White", + "solarized-light": "Solarized Light", + "solarized-dark": "Solarized Dark", + "dracula": "Dracula", + "atom-one-dark": "Atom One Dark", + "black": "Black", + "zenburn": "Zenburn", + "gruvbox-light": "Gruvbox Light", + "gruvbox-dark": "Gruvbox Dark", +} + @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 current_theme = request.cookies.get("theme", "") or user_default_theme - theme_options = { - "white": "White", - "solarized-light": "Solarized Light", - "solarized-dark": "Solarized Dark", - "dracula": "Dracula", - "atom-one-dark": "Atom One Dark", - "black": "Black", - "zenburn": "Zenburn", - "gruvbox-light": "Gruvbox Light", - "gruvbox-dark": "Gruvbox Dark", - } + # Make a copy of the theme options dict so we can add info to the names + theme_options = THEME_OPTIONS.copy() if site_default_theme == user_default_theme: theme_options[site_default_theme] += " (site and account default)" @@ -162,122 +162,97 @@ def post_settings_password_change( 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) + """Generate the theme preview page.""" + # get the generic/unknown user and a random group to display on the example posts + fake_user = request.query(User).filter(User.user_id == -1).one() + group = request.query(Group).order_by(func.random()).limit(1).one() + + fake_link_topic = Topic.create_link_topic( + group, fake_user, "Example Link Topic", "https://tildes.net/" + ) + + fake_text_topic = Topic.create_text_topic( + group, fake_user, "Example Text Topic", "No real text" ) + fake_text_topic.content_metadata = { + "excerpt": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + } - 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 = [fake_link_topic, fake_text_topic] - fake_topics: List[Topic] = [ - Topic.create_link_topic( - fake_group, fake_user, "Example Link Topic", "https://tildes.net/" + # manually add other necessary attributes to the fake topics + for fake_topic in fake_topics: + fake_topic.topic_id = sys.maxsize + fake_topic.tags = ["tag one", "tag two"] + fake_topic.num_comments = 123 + fake_topic.num_votes = 12 + fake_topic.created_time = utc_now() - timedelta(hours=12) + + # create a fake top-level comment that appears to be written by the user + markdown = ( + "This is what a regular comment written by yourself would look like.\n\n" + "It has **formatting** and a [link](https://tildes.net)." + ) + fake_top_comment = Comment(fake_link_topic, request.user, markdown) + fake_top_comment.comment_id = sys.maxsize + fake_top_comment.created_time = utc_now() - timedelta(hours=12, minutes=30) + + child_comments_markdown = [ + ( + "This reply has received an Exemplary label. It also has a blockquote:\n\n" + "> Hello World!" ), - Topic.create_text_topic( - fake_group, fake_user, "Example Text Topic", "empty string" + ( + "This is a reply written by the topic's OP with a code block in it:\n\n" + "```js\n" + "function foo() {\n" + " ['1', '2', '3'].map(parseInt);\n" + "}\n" + "```" + ), + ( + "This reply is new and has the *Mark New Comments* stripe on its left " + "(even if you don't have that feature enabled)." ), ] - 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_comments = [fake_top_comment] + + # vary the ID and created_time on each fake comment so CommentTree works properly + current_comment_id = fake_top_comment.comment_id + current_created_time = fake_top_comment.created_time + for markdown in child_comments_markdown: + current_comment_id -= 1 + current_created_time += timedelta(minutes=5) + + fake_comment = Comment( + fake_link_topic, fake_user, markdown, parent_comment=fake_top_comment ) - ) + fake_comment.comment_id = current_comment_id + fake_comment.created_time = current_created_time + fake_comment.parent_comment_id = fake_top_comment.comment_id + + fake_comments.append(fake_comment) + + # add other necessary attributes to all of the fake comments + for fake_comment in fake_comments: + fake_comment.num_votes = 0 fake_tree = CommentTree( fake_comments, CommentTreeSortOption.RELEVANCE, request.user ) + # add a fake Exemplary label to the first child comment + fake_comments[1].labels = [ + CommentLabel(fake_comments[1], fake_user, CommentLabelOption.EXEMPLARY, 1.0) + ] + + # the comment to mark as new is the last one, so set a visit time just before it + fake_last_visit_time = fake_comments[-1].created_time - timedelta(minutes=1) + return { - **generate_theme_chooser_dict(request), + "theme_options": THEME_OPTIONS, "fake_topics": fake_topics, "fake_comment_tree": fake_tree, - "comment_label_options": [label for label in CommentLabelOption], - "last_visit": fake_last_visit_timestamp, + "last_visit": fake_last_visit_time, }