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);
}
button {
cursor: pointer;
}
code {
display: inline-block;
font-size: inherit;

16
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;
}
}

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/tab";
@import "modules/text";
@import "modules/theme-preview";
@import "modules/time";
@import "modules/toast";
@import "modules/topic";

5
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");
}
@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 {
@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 {
@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 {
@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 {
@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");

16
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"
);

12
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;
}
}
}

5
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");
}
@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=" +
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) {

15
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);
};

1
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")

4
tildes/tildes/templates/settings.jinja2

@ -34,12 +34,12 @@
{% endfor %}
</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">
Set as account default
</button>
</form>
<a href="{{ request.route_url("settings_theme_previews") }}">View theme previews</a>
</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 #}
{% 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 %}
<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">
<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">
{% 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 %}
</div>
</section>
<section class="settings-section">
<h2>Topic listings</h2>
<ol class="topic-listing">
<ol class="topic-listing theme-preview-fake-posts">
{% for fake_topic in fake_topics %}
<li>
{{ render_topic_for_listing(fake_topic, show_group=true) }}
@ -61,17 +36,11 @@
{% endfor %}
</ol>
</section>
<section class="settings-section">
<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>
</section>
{% 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."""
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,
}
Loading…
Cancel
Save