Browse Source

Update/refactor theme preview page

This is mostly just rearranging, but a couple of functional changes:

* The "preview blocks" can now be clicked to switch themes, instead of
  using the dropdown menu.
* Click events should be disabled in the fake posts, so we don't need to
  worry about voting/labels/etc.
merge-requests/85/head
Deimos 5 years ago
parent
commit
4657521373
  1. 4
      tildes/scss/_base.scss
  2. 16
      tildes/scss/modules/_settings.scss
  3. 28
      tildes/scss/modules/_theme-preview.scss
  4. 1
      tildes/scss/styles.scss
  5. 5
      tildes/scss/themes/_atom_one_dark.scss
  6. 5
      tildes/scss/themes/_black.scss
  7. 5
      tildes/scss/themes/_default.scss
  8. 5
      tildes/scss/themes/_dracula.scss
  9. 10
      tildes/scss/themes/_gruvbox.scss
  10. 16
      tildes/scss/themes/_solarized.scss
  11. 12
      tildes/scss/themes/_theme_base.scss
  12. 5
      tildes/scss/themes/_zenburn.scss
  13. 10
      tildes/static/js/behaviors/theme-preview.js
  14. 13
      tildes/static/js/behaviors/theme-selector.js
  15. 15
      tildes/static/js/scripts.js
  16. 1
      tildes/tildes/routes.py
  17. 4
      tildes/tildes/templates/settings.jinja2
  18. 57
      tildes/tildes/templates/settings_theme_previews.jinja2
  19. 215
      tildes/tildes/views/settings.py

4
tildes/scss/_base.scss

@ -35,6 +35,10 @@ body {
@include font-shrink-on-mobile(0.8rem); @include font-shrink-on-mobile(0.8rem);
} }
button {
cursor: pointer;
}
code { code {
display: inline-block; display: inline-block;
font-size: inherit; font-size: inherit;

16
tildes/scss/modules/_settings.scss

@ -28,19 +28,3 @@
border-color: inherit; 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;
}
}

28
tildes/scss/modules/_theme-preview.scss

@ -0,0 +1,28 @@
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
// 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;
}
}

1
tildes/scss/styles.scss

@ -35,6 +35,7 @@
@import "modules/static-site"; @import "modules/static-site";
@import "modules/tab"; @import "modules/tab";
@import "modules/text"; @import "modules/text";
@import "modules/theme-preview";
@import "modules/time"; @import "modules/time";
@import "modules/toast"; @import "modules/toast";
@import "modules/topic"; @import "modules/topic";

5
tildes/scss/themes/_atom_one_dark.scss

@ -47,6 +47,5 @@ $theme-atom-one-dark: (
body.theme-atom-one-dark { body.theme-atom-one-dark {
@include use-theme($theme-atom-one-dark); @include use-theme($theme-atom-one-dark);
} }
body {
@include theme-preview-block($theme-atom-one-dark, "atom-one-dark");
}
@include theme-preview-block($theme-atom-one-dark, "atom-one-dark");

5
tildes/scss/themes/_black.scss

@ -26,6 +26,5 @@ $theme-black: (
body.theme-black { body.theme-black {
@include use-theme($theme-black); @include use-theme($theme-black);
} }
body {
@include theme-preview-block($theme-black, "black");
}
@include theme-preview-block($theme-black, "black");

5
tildes/scss/themes/_default.scss

@ -31,6 +31,5 @@ $default-theme: (
body { body {
@include use-theme($default-theme); @include use-theme($default-theme);
} }
body {
@include theme-preview-block($default-theme, "white");
}
@include theme-preview-block($default-theme, "white");

5
tildes/scss/themes/_dracula.scss

@ -49,6 +49,5 @@ $theme-dracula: (
body.theme-dracula { body.theme-dracula {
@include use-theme($theme-dracula); @include use-theme($theme-dracula);
} }
body {
@include theme-preview-block($theme-dracula, "dracula");
}
@include theme-preview-block($theme-dracula, "dracula");

10
tildes/scss/themes/_gruvbox.scss

@ -110,9 +110,8 @@ $gruvbox-dark: (
body.theme-gruvbox-dark { body.theme-gruvbox-dark {
@include use-theme(map-merge($gruvbox-base, $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 // Light theme definition
$gruvbox-light: ( $gruvbox-light: (
@ -133,6 +132,5 @@ $gruvbox-light: (
body.theme-gruvbox-light { body.theme-gruvbox-light {
@include use-theme(map-merge($gruvbox-base, $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");

16
tildes/scss/themes/_solarized.scss

@ -67,9 +67,11 @@ $solarized-dark: (
body.theme-solarized-dark { body.theme-solarized-dark {
@include use-theme(map-merge($solarized-base, $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 // Light theme definition
$solarized-light: ( $solarized-light: (
@ -87,6 +89,8 @@ $solarized-light: (
body.theme-solarized-light { body.theme-solarized-light {
@include use-theme(map-merge($solarized-base, $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"
);

12
tildes/scss/themes/_theme_base.scss

@ -919,11 +919,9 @@
} }
@mixin theme-preview-block($theme, $name) { @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;
} }
}
}

5
tildes/scss/themes/_zenburn.scss

@ -39,6 +39,5 @@ $theme-zenburn: (
body.theme-zenburn { body.theme-zenburn {
@include use-theme($theme-zenburn); @include use-theme($theme-zenburn);
} }
body {
@include theme-preview-block($theme-zenburn, "zenburn");
}
@include theme-preview-block($theme-zenburn, "zenburn");

10
tildes/static/js/behaviors/theme-preview.js

@ -0,0 +1,10 @@
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
// 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);
});
});

13
tildes/static/js/behaviors/theme-selector.js

@ -25,18 +25,7 @@ $.onmount("[data-js-theme-selector]", function() {
"path=/;max-age=315360000;secure;domain=" + "path=/;max-age=315360000;secure;domain=" +
document.location.hostname; 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 // set visibility of 'Set as account default' button
if (selected_text.indexOf("account default") === -1) { if (selected_text.indexOf("account default") === -1) {

15
tildes/static/js/scripts.js

@ -89,3 +89,18 @@ $(function() {
if (!window.Tildes) { if (!window.Tildes) {
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);
};

1
tildes/tildes/routes.py

@ -18,7 +18,6 @@ from tildes.resources.user import user_by_username
def includeme(config: Configurator) -> None: def includeme(config: Configurator) -> None:
"""Set up application routes.""" """Set up application routes."""
# pylint: disable=too-many-statements
config.add_route("home", "/") config.add_route("home", "/")
config.add_route("search", "/search") config.add_route("search", "/search")

4
tildes/tildes/templates/settings.jinja2

@ -34,12 +34,12 @@
{% endfor %} {% endfor %}
</select> </select>
<a class="btn btn-primary" href="{{ request.route_url('settings_theme_previews') }}">Preview themes</a>
<button id="button-set-default-theme" class="btn btn-link d-none"> <button id="button-set-default-theme" class="btn btn-link d-none">
Set as account default Set as account default
</button> </button>
</form> </form>
<a href="{{ request.route_url("settings_theme_previews") }}">View theme previews</a>
</li> </li>
<li> <li>

57
tildes/tildes/templates/settings_theme_previews.jinja2

@ -1,7 +1,7 @@
{# Copyright (c) 2018 Tildes contributors <code@tildes.net> #}
{# Copyright (c) 2019 Tildes contributors <code@tildes.net> #}
{# SPDX-License-Identifier: AGPL-3.0-or-later #} {# 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 %} {% from 'macros/topics.jinja2' import render_topic_for_listing with context %}
{% extends 'base_settings.jinja2' %} {% extends 'base_settings.jinja2' %}
@ -13,47 +13,22 @@
{% block main_heading %}Theme previews{% endblock %} {% block main_heading %}Theme previews{% endblock %}
{% block settings %} {% block settings %}
<label for="theme">Choose a display theme:</label>
<form
class="form-inline"
name="account-default-theme"
data-ic-patch-to="{{ request.route_url(
'ic_user',
username=request.user.username
) }}"
>
<select class="form-select col-8 col-sm-12" name="theme" id="theme" data-js-theme-selector>
{% for theme, description in theme_options.items() %}
<option
value="{{ theme }}"
{{ 'selected' if theme == current_theme else '' }}
>
{{ description }}
</option>
{% endfor %}
</select>
<a class="btn btn-primary" href="{{ request.route_url('settings') }}">Return to Settings</a>
<button id="button-set-default-theme" class="btn btn-link d-none">
Set as account default
</button>
</form>
<section class="settings-section"> <section class="settings-section">
<h2>Quick overview</h2>
<h2>Theme options</h2>
<p>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 <a href="/settings">the main Settings page</a>.</p>
<div class="theme-preview-blocks"> <div class="theme-preview-blocks">
{% 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 #}
<span class="theme-preview-block-{{ theme }}">{{ description.split(" (")[0]|replace(" ", "\u00a0") }}</span>
{% for class_name, name in theme_options.items() %}
<button
class="theme-preview-block theme-preview-block-{{ class_name }}"
data-js-theme-preview="{{ class_name }}"
>{{ name }}</button>
{% endfor %} {% endfor %}
</div> </div>
</section> </section>
<section class="settings-section"> <section class="settings-section">
<h2>Topic listings</h2> <h2>Topic listings</h2>
<ol class="topic-listing">
<ol class="topic-listing theme-preview-fake-posts">
{% for fake_topic in fake_topics %} {% for fake_topic in fake_topics %}
<li> <li>
{{ render_topic_for_listing(fake_topic, show_group=true) }} {{ render_topic_for_listing(fake_topic, show_group=true) }}
@ -61,17 +36,11 @@
{% endfor %} {% endfor %}
</ol> </ol>
</section> </section>
<section class="settings-section"> <section class="settings-section">
<h2>Comments</h2> <h2>Comments</h2>
<ol class="comment-tree" id="comments">
{{ render_comment_tree(fake_comment_tree, mark_newer_than=last_visit, is_individual_comment=False) }}
<ol class="comment-tree theme-preview-fake-posts">
{{ render_comment_tree(fake_comment_tree, mark_newer_than=last_visit) }}
</ol> </ol>
</section> </section>
{% endblock %} {% endblock %}
{% block templates %}
{% if request.user %}
{{ comment_reply_template() }}
{{ comment_label_options_template(comment_label_options) }}
{% endif %}
{% endblock %}

215
tildes/tildes/views/settings.py

@ -3,8 +3,9 @@
"""Views related to user settings.""" """Views related to user settings."""
from datetime import timedelta
from io import BytesIO from io import BytesIO
from typing import List, Optional
import sys
import pyotp import pyotp
import qrcode import qrcode
@ -12,11 +13,12 @@ from pyramid.httpexceptions import HTTPForbidden, HTTPUnprocessableEntity
from pyramid.request import Request from pyramid.request import Request
from pyramid.response import Response from pyramid.response import Response
from pyramid.view import view_config from pyramid.view import view_config
from sqlalchemy import func
from webargs.pyramidparser import use_kwargs from webargs.pyramidparser import use_kwargs
from tildes.enums import CommentLabelOption, CommentTreeSortOption from tildes.enums import CommentLabelOption, CommentTreeSortOption
from tildes.lib.datetime import utc_now
from tildes.lib.string import separate_string 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.comment import Comment, CommentLabel, CommentTree
from tildes.models.group import Group from tildes.models.group import Group
from tildes.models.topic import Topic from tildes.models.topic import Topic
@ -30,31 +32,29 @@ from tildes.schemas.user import (
PASSWORD_FIELD = UserSchema(only=("password",)).fields["password"] 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") @view_config(route_name="settings", renderer="settings.jinja2")
def get_settings(request: Request) -> dict: def get_settings(request: Request) -> dict:
"""Generate the user settings page.""" """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" site_default_theme = "white"
user_default_theme = request.user.theme_default or site_default_theme user_default_theme = request.user.theme_default or site_default_theme
current_theme = request.cookies.get("theme", "") or user_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: if site_default_theme == user_default_theme:
theme_options[site_default_theme] += " (site and account default)" 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" route_name="settings_theme_previews", renderer="settings_theme_previews.jinja2"
) )
def get_settings_theme_previews(request: Request) -> dict: 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_tree = CommentTree(
fake_comments, CommentTreeSortOption.RELEVANCE, request.user 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 { return {
**generate_theme_chooser_dict(request),
"theme_options": THEME_OPTIONS,
"fake_topics": fake_topics, "fake_topics": fake_topics,
"fake_comment_tree": fake_tree, "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,
} }
Loading…
Cancel
Save