diff options
24 files changed, 2524 insertions, 0 deletions
| diff --git a/src/application/templates/modern/base.twig b/src/application/templates/modern/base.twig new file mode 100644 index 0000000..819af80 --- /dev/null +++ b/src/application/templates/modern/base.twig @@ -0,0 +1,222 @@ +{%- if title -%} +    {%- set title = title ~ " | " -%} +{%- endif -%} +{%- set title = title ~ (g.env.MYSTIC_FORUM_TITLE|default("Forum")) -%} +{%- set nextParam = "" -%} +{%- if g.globals.action in ["login", "register"] -%} +    {%- set nextParam = g.get.next|default("") -%} +{%- else -%} +    {%- set nextParam = g.server.REQUEST_URI -%} +{%- endif -%} +<!DOCTYPE html> +<html lang="{{ __("en", context: "HTML language") }}"> +<head> +    <meta charset="utf-8"> +    <meta name="color-scheme" content="light dark"> +    <meta name="viewport" content="width=device-width,initial-scale=1"> +    <meta name="generator" content="mysticBB {{ constant("MYSTICBB_VERSION") }}"> +    <title>{{ title }}</title> +    <link rel="stylesheet" href=".?_action=ctheme"> +    <script src=".?_action=ji18n"></script> +    {% block head_after %}{% endblock %} +</head> +<body> + +{% block nav %} +<header> +    <a id="brand" href=".">{{ g.env.MYSTIC_FORUM_TITLE|default("Forum") }}</a> +    <nav> +        {% block navbar %} +            {% if currentUser %} +                <a aria-label="{{ __("Search") }}" href="?_action=search"{{ g.globals.action == "search" ? ' class="active"'|raw : '' }}> +                    <svg viewBox="0 0 24 24" class="icon"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg> +                </a> +                <a aria-label="{{ __("View profile") }}" href="?_action=viewuser&user={{ currentUser.id|url_encode }}"{{ (g.globals.action == "viewuser" and g.get.user == currentUser.id) ? ' class="active"'|raw : '' }}> +                    <svg viewBox="0 0 24 24" class="icon"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> +                </a> +                <a aria-label="{{ __("Log out") }}" href="?_action=logout&next={{ g.server.REQUEST_URI|url_encode }}"> +                    <svg viewBox="0 0 24 24" class="icon"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg> +                </a> +            {% else %} +                <a aria-label="{{ __("Search") }}" href="?_action=search"{{ g.globals.action == "search" ? ' class="active"'|raw : '' }}> +                    <svg viewBox="0 0 24 24" class="icon"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg> +                </a> +                <a href="?_action=auth&next={{ nextParam|url_encode }}"{{ g.globals.action == "auth" ? ' class="active"'|raw : '' }}>{{ __("Log in") }}</a> +                {% if constant("REGISTRATION_ENABLED") %} +                    <a href="?_action=register&next={{ nextParam|url_encode }}"{{ g.globals.action == "register" ? ' class="active"'|raw : '' }}>{{ __("Register") }}</a> +                {% endif %} +            {% endif %} +        {% endblock %} +    </nav> +</header> +{% endblock %} + +{% block main %} +<main> +    {% block content %}{% endblock %} +</main> +{% endblock %} + +{% block footer %} +<footer> +    <div id="about"> +        © {{ "now"|date("Y") }} {{ g.env.MYSTIC_FORUM_COPYRIGHT|default(g.env.MYSTIC_FORUM_TITLE)|default("Forum") }}. +        Powered by <a href="https://git.jkohl.link/mystic-forum.git/tag/?h=v{{ constant("MYSTICBB_VERSION")|url_encode }}">mysticBB v{{ constant("MYSTICBB_VERSION") }}</a>. +    </div> +    <div id="preferences"> +        <form action="?_action=settheme" class="form-inline" method="post"> +            <input type="hidden" name="next" value="{{ g.server.REQUEST_URI }}"> +            <div class="form-group"> +                <label for="theme-select">{{ __("Theme:") }}</label> +                <select id="theme-select" name="theme" onchange="this.form.submit()"> +                    {% for themeKey, themeInfo in availableThemes %} +                        <option value="{{ themeKey }}"{{ themeKey == currentTheme ? " selected" : "" }}>{{ themeInfo.name }}</option> +                    {% endfor %} +                </select> +            </div> +        </form> +        <form action="?_action=setlang" class="form-inline" method="post"> +            <input type="hidden" name="next" value="{{ g.server.REQUEST_URI }}"> +            <div class="form-group"> +                <label for="lang-select">{{ __("Language:") }}</label> +                <select id="lang-select" name="lang" onchange="this.form.submit()"> +                    {% for langKey, langName in availableLangs %} +                        <option value="{{ langKey }}"{{ langKey == currentLang ? " selected" : "" }}>{{ langName }}</option> +                    {% endfor %} +                </select> +            </div> +        </form> +    </div> +</footer> +{% endblock %} + +{% block scripts %} +<script type="text/javascript"> +    (function() { +        function insertAroundSelection(textarea, before, after) { +            var start = textarea.selectionStart; +            var end = textarea.selectionEnd; +            var text = textarea.value; +            var pre = text.substring(0, start); +            var inner = text.substring(start, end); +            var post = text.substring(end); +            start += before.length; +            end += before.length; +            text = pre + before + inner + after + post; +            textarea.value = text; +            textarea.focus(); +            textarea.selectionStart = start; +            textarea.selectionEnd = end; +        } + +        function getTextarea(btn) { +            return document.querySelector(btn.dataset.area); +        } + +        var commands = { +            bold: function(textarea) { +                insertAroundSelection(textarea, "[b]", "[/b]"); +            }, +            italic: function(textarea) { +                insertAroundSelection(textarea, "[i]", "[/i]"); +            }, +            underline: function(textarea) { +                insertAroundSelection(textarea, "[u]", "[/u]"); +            }, +            strikethrough: function(textarea) { +                insertAroundSelection(textarea, "[s]", "[/s]"); +            }, +            sup: function(textarea) { +                insertAroundSelection(textarea, "[^]", "[/^]"); +            }, +            sub: function(textarea) { +                insertAroundSelection(textarea, "[_]", "[/_]"); +            }, +            quote: function(textarea) { +                insertAroundSelection(textarea, "> ", ""); +            }, +            spoiler: function(textarea) { +                insertAroundSelection(textarea, "[spoiler]", "[/spoiler]"); +            } +        } + +        document.querySelectorAll("button[data-editor-command]").forEach(e => { +            e.addEventListener("click", function() { +                const command = e.dataset.editorCommand; +                const textarea = getTextarea(e); +                commands[command](textarea); +            }); +        }); +    })(); +</script> + +<script> +    document.querySelectorAll("._time").forEach(e => { +        const date = new Date(e.textContent.trim()); +        e.textContent = date.toLocaleString(); +    }); +    document.querySelectorAll("._date").forEach(e => { +        const date = new Date(e.textContent.trim()); +        e.textContent = date.toLocaleDateString(); +    }); +    document.querySelectorAll("._time-only").forEach(e => { +        const date = new Date(e.textContent.trim()); +        e.textContent = date.toLocaleTimeString(); +    }); +    document.querySelectorAll("[data-dismiss='modal']").forEach(e => { +        e.addEventListener("click", () => { +            const modal = e.closest(".modal"); +            if (!modal) +                return; +            modal.classList.add("fade"); +            modal.dispatchEvent(new Event("hidemodal")); +        }); +    }) +</script> + +<script> +    // feature detection +    (function() { +        var unsupportedFeatures = []; + +        function notSupported(feat) { +            unsupportedFeatures.push("- " + feat); +        } + +        var supports = { +            cssNesting: ((function() { +                try { +                    document.querySelector("&"); +                    return true; +                } catch (e) { +                    return false; +                } +            })()), +            es6Syntax: ((function() { +                try { +                    Function("() => {};"); +                    return true; +                } catch (e) { +                    return false; +                } +            })()) +        }; + +        if (!supports.cssNesting) notSupported("CSS nesting"); +        if (!supports.es6Syntax) notSupported("ECMAScript 6 Syntax"); + +        if (unsupportedFeatures.length > 0) { +            alert( +                "ATTENTION: This site might not work properly because your " + +                "browser does not support some required features: \n\n" + +                unsupportedFeatures.join("\n") + +                "\n\n" + +                "Please try chaning the theme to something else!" +            ); +        } +    })(); +</script> +{% endblock %} + +</body> +</html> diff --git a/src/application/templates/modern/components/alert_error.twig b/src/application/templates/modern/components/alert_error.twig new file mode 100644 index 0000000..ba14ba5 --- /dev/null +++ b/src/application/templates/modern/components/alert_error.twig @@ -0,0 +1,8 @@ +<div class="alert alert-danger" role="alert"> +    <svg viewBox="0 0 24 24" class="icon"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg> +    {% if message starts with "?!HTML::" %} +        {{ message|slice(8)|raw }} +    {% else %} +        <span>{{ message }}</span> +    {% endif %} +</div> diff --git a/src/application/templates/modern/components/alert_info.twig b/src/application/templates/modern/components/alert_info.twig new file mode 100644 index 0000000..6559ac2 --- /dev/null +++ b/src/application/templates/modern/components/alert_info.twig @@ -0,0 +1,8 @@ +<div class="alert alert-info" role="alert"> +    <svg viewBox="0 0 24 24" class="icon"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> +    {% if message starts with "?!HTML::" %} +        {{ message|slice(8)|raw }} +    {% else %} +        <span>{{ message }}</span> +    {% endif %} +</div> diff --git a/src/application/templates/modern/components/alert_success.twig b/src/application/templates/modern/components/alert_success.twig new file mode 100644 index 0000000..ed64ce5 --- /dev/null +++ b/src/application/templates/modern/components/alert_success.twig @@ -0,0 +1,8 @@ +<div class="alert alert-success" role="alert"> +    <svg viewBox="0 0 24 24" class="icon"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg> +    {% if message starts with "?!HTML::" %} +        {{ message|slice(8)|raw }} +    {% else %} +        <span>{{ message }}</span> +    {% endif %} +</div> diff --git a/src/application/templates/modern/components/post.twig b/src/application/templates/modern/components/post.twig new file mode 100644 index 0000000..5ceb46e --- /dev/null +++ b/src/application/templates/modern/components/post.twig @@ -0,0 +1,180 @@ +{%- set fileAttachments = attachments|filter(a => not (a.mimeType starts with "image/" or a.mimeType starts with "video/")) -%} +{%- set imageAttachments = attachments|filter(a => a.mimeType starts with "image/" or a.mimeType starts with "video/") -%} + +{%- set canReply = +    not post.deleted +    and not topic.isLocked +    and currentUser is not null +    and currentUser.hasPermission(permission("CREATE_OWN_POST")) +-%} + +{%- set canEdit = +    not post.deleted +    and not topic.isLocked +    and currentUser is not null +    and ( +        ( +            postAuthor is not null +            and postAuthor.id == currentUser.id +            and postAuthor.hasPermission(permission("EDIT_OWN_POST")) +        ) +        or currentUser.hasPermission(permission("EDIT_OTHER_POST")) +    ) -%} + +{%- set canDelete = +    not post.deleted +    and currentUser is not null +    and ( +        ( +            postAuthor is not null +            and postAuthor.id == currentUser.id +            and postAuthor.hasPermission(permission("DELETE_OWN_POST")) +        ) +        or currentUser.hasPermission(permission("DELETE_OTHER_POST")) +    ) -%} + +{%- set canViewAttachments = currentUser is not null -%} + +{%- set your_are_the_author = +    currentUser is not null +    and postAuthor is not null +    and currentUser.id == postAuthor.id +-%} + +{%- set is_op = +    topicAuthor is not null +    and postAuthor is not null +    and postAuthor.id == topicAuthor.id +-%} + +{% if post.deleted %} +    <div class="post" id="post-{{ post.id }}"> +        <div class="post-pfp"> +            <div class="media-object" style="width:64px"></div> +        </div> +        <div class="post-body"> +            <div class="post-status post-status-danger"> +                <svg viewBox="0 0 24 24" class="icon"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg> +                <em>{{ __("This post has been deleted") }}</em> +            </div> +        </div> +    </div> +{% else %} +    <div class="post" data-text="{{ post.content }}" style="overflow: visible;" data-post-id="{{ post.id }}"> +        <div id="post-{{ post.id }}" class="post-anchor"></div> +        {% if not hide_pfp %} +            <div class="post-pfp"> +                {% if postAuthor %} +                    {% if hide_actions %} +                        <img class="media-object" alt="{{ __("Profile picture") }}" src="?_action=profilepicture&user={{ postAuthor.id|url_encode }}" width="64" height="64"> +                    {% else %} +                        <a href="?_action=viewuser&user={{ postAuthor.id|url_encode }}"> +                            <img class="media-object" alt="{{ __("Profile picture") }}" src="?_action=profilepicture&user={{ postAuthor.id|url_encode }}" width="64" height="64"> +                        </a> +                    {% endif %} +                {% else %} +                    <div class="media-object" style="width:64px;height:64px"></div> +                {% endif %} +            </div> +        {% endif %} +        <div class="post-body" style="overflow: visible;"> +            <div class="post-title"> +                {% if not hide_actions %} +                    <div class="pull-right title-controls"> +                        <a href="#post-{{ post.id }}" class="btn btn-iconic" title="{{ __("Permalink") }}"> +                            <svg viewBox="0 0 24 24" class="icon"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg> +                        </a> +                        {% if canReply %} +                            <button data-post-id="{{ post.id }}" class="btn js-only _reply-post btn-iconic" title="{{ __("Reply to post") }}"> +                                <svg viewBox="0 0 24 24" class="icon"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg> +                            </button> +                        {% endif %} +                        {% if canEdit %} +                            <button data-post-id="{{ post.id }}" class="btn js-only _edit-post btn-iconic" title="{{ __("Edit post") }}"> +                                <svg viewBox="0 0 24 24" class="icon"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg> +                            </button> +                        {% endif %} +                        {% if canDelete %} +                            <form action="?_action=deletepost" method="post"> +                                <input type="hidden" name="post" value="{{ post.id }}"> +                                <button type="submit" class="btn btn-danger btn-iconic" title="{{ __("Delete post") }}"> +                                    <svg viewBox="0 0 24 24" class="icon"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg> +                                </button> +                            </form> +                        {% endif %} +                    </div> +                {% endif %} +                {% if postAuthor %} +                    {% if hide_actions %} +                        {{ postAuthor.displayName }} +                    {% else %} +                        <a href="?_action=viewuser&user={{ postAuthor.id|url_encode }}">{{ postAuthor.displayName }}</a> +                        {% if is_op %} +                            <svg viewBox="0 0 24 24" class="icon is-op icon-in-text"><title>{{ __("Created this topic") }}</title><path d="m11 7.601-5.994 8.19a1 1 0 0 0 .1 1.298l.817.818a1 1 0 0 0 1.314.087L15.09 12"/><path d="M16.5 21.174C15.5 20.5 14.372 20 13 20c-2.058 0-3.928 2.356-6 2-2.072-.356-2.775-3.369-1.5-4.5"/><circle cx="16" cy="7" r="5"/></svg> +                        {% endif %} +                    {% endif %} +                    {% if your_are_the_author %} +                        <svg viewBox="0 0 24 24" class="icon is-you icon-in-text"><title>{{ __("You") }}</title><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> +                    {% endif %} +                {% else %} +                    <em class="text-muted">{{ __("(deleted)") }}</em> +                {% endif %} +                <div class="post-info"> +                    <span class="_time">{{ post.postDate.format("c") }}</span> +                    {% if post.edited %} +                        <em class="text-muted">{{ __("(edited)") }}</em> +                    {% endif %} +                </div> +            </div> +            <div class="post-main"> +                <div class="post-content">{{ renderPost(post.content) }}</div> +                {% if imageAttachments|length > 0 %} +                    <div class="post-images"> +                        {% for attachment in imageAttachments %} +                            {% if hide_actions %} +                                <span class="image-attachment" title="{{ attachment.name }}"> +                                    <img class="image-attachment-image" src="?_action=thumb&attachment={{ attachment.id|url_encode }}" alt="" width="100"> +                                </span> +                            {% else %} +                                <a class=" +                                    image-attachment +                                    attachment +                                    {{ attachment.mimeType starts with "video/" ? "video-attachment" }} +                                " href="?_action=attachment&attachment={{ attachment.id|url_encode }}" title="{{ attachment.name }}" data-attachment-id="{{ attachment.id }}"> +                                    <img class="image-attachment-image" src="?_action=thumb&attachment={{ attachment.id|url_encode }}" alt="" width="100"> +                                    {% if not canViewAttachments %} +                                        <svg viewBox="0 0 24 24" class="attachment-lock icon"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> +                                    {% elseif attachment.mimeType starts with "video/" %} +                                        <svg viewBox="0 0 24 24" class="video-player-icon icon"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg> +                                    {% endif %} +                                </a> +                            {% endif %} +                        {% endfor %} +                    </div> +                {% endif %} +            </div> +            {% if fileAttachments|length > 0 %} +                <div class="post-attachments"> +                    <div class="btn-toolbar"> +                        <div class="btn-group"> +                            {% for attachment in fileAttachments %} +                                {% if hide_actions %} +                                    <button>{{ attachment.name }}</button> +                                {% else %} +                                    <a class="btn attachment" href="?_action=attachment&attachment={{ attachment.id|url_encode }}"> +                                        {% if not canViewAttachments %} +                                            <svg viewBox="0 0 24 24" class="icon"><circle cx="12" cy="12" r="10"/><path d="m4.9 4.9 14.2 14.2"/></svg> +                                        {% else %} +                                            <svg viewBox="0 0 24 24" class="icon"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg> +                                        {% endif %} +                                        <span>{{ attachment.name }}</span> +                                    </a> +                                {% endif %} +                            {% endfor %} +                        </div> +                    </div> +                </div> +            {% endif %} +        </div> +    </div> +{% endif %} diff --git a/src/application/templates/modern/components/richtext_editor.twig b/src/application/templates/modern/components/richtext_editor.twig new file mode 100644 index 0000000..3a2874d --- /dev/null +++ b/src/application/templates/modern/components/richtext_editor.twig @@ -0,0 +1,35 @@ +<div class="richtext-editor"> +    <div class="rt-toolbar" role="toolbar"> +        <div class="rt-group" role="group"> +            <button data-area="#{{ id }}" title="{{ __("Bold") }}" data-editor-command="bold" type="button" class="btn btn-iconic"> +                <svg viewBox="0 0 24 24" class="icon"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/></svg> +            </button> +            <button data-area="#{{ id }}" title="{{ __("Italic") }}" data-editor-command="italic" type="button" class="btn btn-iconic"> +                <svg viewBox="0 0 24 24" class="icon"><line x1="19" x2="10" y1="4" y2="4"/><line x1="14" x2="5" y1="20" y2="20"/><line x1="15" x2="9" y1="4" y2="20"/></svg> +            </button> +            <button data-area="#{{ id }}" title="{{ __("Underlined") }}" data-editor-command="underline" type="button" class="btn btn-iconic"> +                <svg viewBox="0 0 24 24" class="icon"><path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="4" x2="20" y1="20" y2="20"/></svg> +            </button> +            <button data-area="#{{ id }}" title="{{ __("Strikethrough") }}" data-editor-command="strikethrough" type="button" class="btn btn-iconic"> +                <svg viewBox="0 0 24 24" class="icon"><path d="M16 4H9a3 3 0 0 0-2.83 4"/><path d="M14 12a4 4 0 0 1 0 8H6"/><line x1="4" x2="20" y1="12" y2="12"/></svg> +            </button> +        </div> +        <div class="rt-group" role="group"> +            <button data-area="#{{ id }}" title="{{ __("Superscript") }}" data-editor-command="sup" type="button" class="btn btn-iconic"> +                <svg viewBox="0 0 24 24" class="icon"><path d="m4 19 8-8"/><path d="m12 19-8-8"/><path d="M20 12h-4c0-1.5.442-2 1.5-2.5S20 8.334 20 7.002c0-.472-.17-.93-.484-1.29a2.105 2.105 0 0 0-2.617-.436c-.42.239-.738.614-.899 1.06"/></svg> +            </button> +            <button data-area="#{{ id }}" title="{{ __("Subscript") }}" data-editor-command="sub" type="button" class="btn btn-iconic"> +                <svg viewBox="0 0 24 24" class="icon"><path d="m4 5 8 8"/><path d="m12 5-8 8"/><path d="M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"/></svg> +            </button> +        </div> +        <div class="rt-group" role="group"> +            <button data-area="#{{ id }}" title="{{ __("Quote") }}" data-editor-command="quote" type="button" class="btn btn-iconic"> +                <svg viewBox="0 0 24 24" class="icon"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg> +            </button> +            <button data-area="#{{ id }}" title="{{ __("Spoiler") }}" data-editor-command="spoiler" type="button" class="btn btn-iconic"> +                <svg viewBox="0 0 24 24" class="icon"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg> +            </button> +        </div> +    </div> +    <textarea class="form-control" id="{{ id }}" name="{{ name }}" required rows="12" cols="60"></textarea> +</div> diff --git a/src/application/templates/modern/components/topic_log.twig b/src/application/templates/modern/components/topic_log.twig new file mode 100644 index 0000000..853dc20 --- /dev/null +++ b/src/application/templates/modern/components/topic_log.twig @@ -0,0 +1,68 @@ +{%- set user = "" -%} +{%- if postAuthor is null -%} +    {%- set user = __("(deleted)")|e("html") -%} +{%- else -%} +    {%- set user = +        '<a href="?_action=viewuser&user=' +        ~ postAuthor.id|url_encode|e("html") +        ~ '">' +        ~ postAuthor.displayName|e("html") +        ~ '</a>' +    -%} +{%- endif -%} + +<div class="post log" id="post-{{ logMessage.id }}"> +    <div class="post-pfp"> +        {% if postAuthor %} +            {% if hideActions %} +                <img class="media-object" alt="{{ __("Profile picture") }}" src="?_action=profilepicture&user={{ postAuthor.id|url_encode }}" width="64" height="64"> +            {% else %} +                <a href="?_action=viewuser&user={{ postAuthor.id|url_encode }}"> +                    <img class="media-object" alt="{{ __("Profile picture") }}" src="?_action=profilepicture&user={{ postAuthor.id|url_encode }}" width="64" height="64"> +                </a> +            {% endif %} +        {% else %} +            <div class="media-object" style="width:64px;height:64px"></div> +        {% endif %} +    </div> +    <div class="post-body"> +        {% if logMessage.type == constant("mystic\\forum\\orm\\TopicLogMessage::LOCKED") %} +            <div class="post-status post-status-warning"> +                <svg viewBox="0 0 24 24" class="icon"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> +                <span> +                    <em>{{ __("%user% locked this topic", { +                        "user": user, +                    }) }}</em> +                    <br> +                    <small class="_time">{{ logMessage.postDate.format("c") }}</small> +                </span> +            </div> +        {% elseif logMessage.type == constant("mystic\\forum\\orm\\TopicLogMessage::UNLOCKED") %} +            <div class="post-status post-status-success"> +                <svg viewBox="0 0 24 24" class="icon"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg> +                <span> +                    <em>{{ __("%user% unlocked this topic", { +                        "user": user, +                    }) }}</em> +                    <br> +                    <small class="_time">{{ logMessage.postDate.format("c") }}</small> +                </span> +            </div> +        {% elseif logMessage.type == constant("mystic\\forum\\orm\\TopicLogMessage::TITLE_CHANGED") %} +            <div class="post-status post-status-info"> +                <svg viewBox="0 0 24 24" class="icon"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg> +                <span> +                    <em>{{ __("%user% changed the title of this topic from %old_title% to %new_title%", { +                        "user": user, +                        "old_title": '<strong>' ~ logMessage.params.old_value|default(__("unknown"))|e("html") ~ '</strong>', +                        "new_title": '<strong>' ~ logMessage.params.new_value|default(__("unknown"))|e("html") ~ '</strong>', +                    }) }}</em> +                    <br> +                    <small class="_time">{{ logMessage.postDate.format("c") }}</small> +                </span> +            </div> +        {% else %} +            {{ __("unknown") }} +        {% endif %} +    </div> +</div> diff --git a/src/application/templates/modern/delete_post.twig b/src/application/templates/modern/delete_post.twig new file mode 100644 index 0000000..e37841a --- /dev/null +++ b/src/application/templates/modern/delete_post.twig @@ -0,0 +1,40 @@ +{% set title = __("Delete post") %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header"> +    <h1>{{ __("Do you want to delete this post?") }}</h1> +</div> + +<div class="alert alert-danger"> +    <svg viewBox="0 0 24 24" class="icon"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg> +    <span>{{ __("Are you sure you want to delete the following post:") }}</span> +</div> +{% include "components/post.twig" with { +    post: ctx.post, +    postAuthor: ctx.postAuthor, +    attachments: ctx.attachments, +    hide_actions: true, +} %} +<div class="page-actions"> +    <form action=".#post-{{ ctx.post.id }}" method="get" class="seamless-inline"> +        <input type="hidden" name="_action" value="viewtopic"> +        <input type="hidden" name="topic" value="{{ ctx.post.topicId }}"> +        <button> +            <svg viewBox="0 0 24 24" class="icon"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg> +            <span>{{ __("Keep post") }}</span> +        </button> +    </form> +    <form action="?_action=deletepost" method="post" class="seamless-inline"> +        <input type="hidden" name="post" value="{{ ctx.post.id }}"> +        <input type="hidden" name="confirm" value="{{ ("confirm" ~ ctx.post.id)|hash("sha256", true)|base64_encode }}"> +        <button class="btn-danger"> +            <svg viewBox="0 0 24 24" class="icon"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg> +            <span>{{ __("Delete post") }}</span> +        </button> +    </form> +</div> + +{% endblock %} diff --git a/src/application/templates/modern/delete_topic.twig b/src/application/templates/modern/delete_topic.twig new file mode 100644 index 0000000..51acaa8 --- /dev/null +++ b/src/application/templates/modern/delete_topic.twig @@ -0,0 +1,37 @@ +{% set title = __("Delete topic") %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header"> +    <h1>{{ __("Do you want to delete this topic?") }}</h1> +</div> + +<div class="alert alert-danger"> +    <svg viewBox="0 0 24 24" class="icon"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg> +    <span>{{ __("Are you sure you want to delete the topic <em>%topic%</em> including <strong>all posts and attachments</strong>?", { +        "topic": ctx.topic.title|e("html"), +    }) }}</span> +</div> + +<div class="page-actions"> +    <form action="." method="get" class="seamless-inline"> +        <input type="hidden" name="_action" value="viewtopic"> +        <input type="hidden" name="topic" value="{{ ctx.topic.id }}"> +        <button> +            <svg viewBox="0 0 24 24" class="icon"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg> +            <span>{{ __("Keep topic") }}</span> +        </button> +    </form> +    <form action="?_action=deletetopic" method="post" class="seamless-inline"> +        <input type="hidden" name="topic" value="{{ ctx.topic.id }}"> +        <input type="hidden" name="confirm" value="{{ ("confirm" ~ ctx.topic.id)|hash("sha256", true)|base64_encode }}"> +        <button class="btn-danger"> +            <svg viewBox="0 0 24 24" class="icon"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg> +            <span>{{ __("Delete topic & posts") }}</span> +        </button> +    </form> +</div> + +{% endblock %} diff --git a/src/application/templates/modern/error_page.twig b/src/application/templates/modern/error_page.twig new file mode 100644 index 0000000..dde4057 --- /dev/null +++ b/src/application/templates/modern/error_page.twig @@ -0,0 +1,12 @@ +{% set title = __("Error") %} +{% extends "base.twig" %} + +{% block navbar %} +    {% if not ctx.skipLoginCheck %} +        {{ parent() }} +    {% endif %} +{% endblock %} + +{% block content %} +    {% include "components/alert_error.twig" with { message: ctx.message } %} +{% endblock %} diff --git a/src/application/templates/modern/info_page.twig b/src/application/templates/modern/info_page.twig new file mode 100644 index 0000000..e243b36 --- /dev/null +++ b/src/application/templates/modern/info_page.twig @@ -0,0 +1,12 @@ +{% set title = __("Information") %} +{% extends "base.twig" %} + +{% block navbar %} +    {% if not ctx.skipLoginCheck %} +        {{ parent() }} +    {% endif %} +{% endblock %} + +{% block content %} +    {% include "components/alert_info.twig" with { message: ctx.message } %} +{% endblock %} diff --git a/src/application/templates/modern/login.twig b/src/application/templates/modern/login.twig new file mode 100644 index 0000000..83b3f59 --- /dev/null +++ b/src/application/templates/modern/login.twig @@ -0,0 +1,46 @@ +{% set title = __("Log in") %} +{% set formId = "login" %} +{% set formError = getAndClearFormError(formId) %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header"> +    <h1>{{ __("Log in") }}</h1> +</div> + +<div class="main-form"> +    {% if formError %} +        {% include "components/alert_error.twig" with { message: formError } %} +    {% endif %} +    <form action="{{ g.server.REQUEST_URI }}" method="post"> +        <input type="hidden" name="form_id" value="{{ formId }}"> + +        <div class="form-group"> +            <label for="i_username">{{ __("Username:") }}</label> +            <input class="form-control" type="text" id="i_username" name="username" value="{{ lastFormField(formId, "username") }}" required autofocus> +        </div> + +        <div class="form-group"> +            <label for="i_password">{{ __("Password:") }}</label> +            <input class="form-control" type="password" id="i_password" name="password" required> +        </div> + +        <div class="form-group form-actions"> +            <button class="btn btn-primary" type="submit">{{ __("Log in") }}</button> +            <a class="btn btn-link" href="?_action=pwreset">{{ __("I forgot my password") }}</a> +        </div> + +        {% if constant("REGISTRATION_ENABLED") %} +            <div class="form-group form-additional"> +                {{ __("Don't have an account? %link%Register now%/link%", { +                    "link": '<a href="?_action=register">', +                    "/link": '</a>', +                }) }} +            </div> +        {% endif %} +    </form> +</div> + +{% endblock %} diff --git a/src/application/templates/modern/new_password.twig b/src/application/templates/modern/new_password.twig new file mode 100644 index 0000000..64854b6 --- /dev/null +++ b/src/application/templates/modern/new_password.twig @@ -0,0 +1,38 @@ +{% set title = __("Reset password") %} +{% set formId = "pwnew" %} +{% set formError = getAndClearFormError(formId) %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header"> +    <h1>{{ __("Reset password") }}</h1> +</div> + +<div class="main-form"> +    {% if formError %} +        {% include "components/alert_error.twig" with { message: formError } %} +    {% endif %} +    <form action="{{ g.server.REQUEST_URI }}" method="post"> +        <input type="hidden" name="form_id" value="{{ formId }}"> +        <input type="hidden" name="token" value="{{ g.get.token }}"> +        <input type="hidden" name="sig" value="{{ g.get.sig }}"> +         +        <div class="form-group"> +            <label for="i_new_password">{{ __("New password:") }}</label> +            <input class="form-control" type="password" id="i_new_password" name="new_password" required autofocus> +        </div> + +        <div class="form-group"> +            <label for="i_retype_password">{{ __("Retype password:") }}</label> +            <input class="form-control" type="password" id="i_retype_password" name="retype_password" required> +        </div> + +        <div class="form-group form-actions"> +            <button class="btn btn-primary" type="submit">{{ __("Set new password") }}</button> +        </div> +    </form> +</div> + +{% endblock %} diff --git a/src/application/templates/modern/new_topic.twig b/src/application/templates/modern/new_topic.twig new file mode 100644 index 0000000..9cd18c9 --- /dev/null +++ b/src/application/templates/modern/new_topic.twig @@ -0,0 +1,39 @@ +{% set title = __("New topic") %} +{% set formId = "newtopic" %} +{% set formError = getAndClearFormError(formId) %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header"> +    <h1>{{ __("New topic") }}</h1> +</div> + +{% if formError %} +    {% include "components/alert_error.twig" with { message: formError } %} +{% endif %} +<form action="{{ g.server.REQUEST_URI }}" method="post" enctype="multipart/form-data"> +    <input type="hidden" name="form_id" value="{{ formId }}"> +    <div class="form-group"> +        <label for="i_title">{{ __("Topic title:") }}</label> +        <input type="text" id="i_title" name="title" value="{{ lastFormField(formId, "title") }}" required autofocus> +    </div> +    <div class="form-group"> +        <label for="i_message">{{ __("Message:") }}</label> +        {% include "components/richtext_editor.twig" with { id: "i_message", name: "message" } %} +    </div> +    <div class="form-group"> +        <label for="i_files">{{ __("Attachments: <small>(max. %max_attachment_count% files, max. %max_attachment_size% MiB each)</small>", { +            "max_attachment_count": constant("MAX_ATTACHMENT_COUNT"), +            "max_attachment_size": constant("MAX_ATTACHMENT_SIZE") // (2**20), +        }) }}</label> +        <input type="file" name="files[]" id="i_files" multiple accept="*/*"> +    </div> +    <button type="submit" class="btn btn-success"> +        <span>{{ __("Create topic") }}</span> +        <svg viewBox="0 0 24 24" class="icon"><path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/></svg> +    </button> +</form> + +{% endblock %} diff --git a/src/application/templates/modern/password_reset.twig b/src/application/templates/modern/password_reset.twig new file mode 100644 index 0000000..4ed9bbe --- /dev/null +++ b/src/application/templates/modern/password_reset.twig @@ -0,0 +1,38 @@ +{% set title = __("Reset password") %} +{% set formId = "pwreset" %} +{% set formError = getAndClearFormError(formId) %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header"> +    <h1>{{ __("Reset password") }}</h1> +</div> + +<div class="main-form"> +    {% if formError %} +        {% include "components/alert_error.twig" with { message: formError } %} +    {% endif %} +    <form action="{{ g.server.REQUEST_URI }}" method="post"> +        <input type="hidden" name="form_id" value="{{ formId }}"> + +        <div class="form-group"> +            <label for="i_username">{{ __("Email address:") }}</label> +            <input class="form-control" type="email" id="i_email" name="email" value="{{ lastFormField(formId, "email") }}" required autofocus> +        </div> + +        <div class="form-group form-actions"> +            <button class="btn btn-primary" type="submit">{{ __("Reset password") }}</button> +        </div> + +        <div class="form-group form-additional"> +            {{ __("I know my password and I want to %link%log in%/link%!", { +                "link": '<a href="?_action=auth">', +                "/link": '</a>', +            }) }} +        </div> +    </form> +</div> + +{% endblock %} diff --git a/src/application/templates/modern/register.twig b/src/application/templates/modern/register.twig new file mode 100644 index 0000000..8e7dfe5 --- /dev/null +++ b/src/application/templates/modern/register.twig @@ -0,0 +1,92 @@ +{% set title = __("Register") %} +{% set formId = "register" %} +{% set formError = getAndClearFormError(formId) %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header"> +    <h1>{{ __("Register") }}</h1> +</div> + +<div class="main-form"> +    {% if formError %} +        {% include "components/alert_error.twig" with { message: formError } %} +    {% endif %} +    <form action="{{ g.server.REQUEST_URI }}" method="post"> +        <input type="hidden" name="form_id" value="{{ formId }}"> + +        <div class="form-group" id="group0"> +            <label for="i_username">{{ __("Username:") }}</label> +            <input class="form-control" id="i_username" type="text" name="username" value="" required> +        </div> + +        <div class="form-group" id="group1"> +            <label for="i_username">{{ __("Username:") }}</label> +            <input class="form-control" id="i_username" type="text" name="df82a9bc21" value="{{ lastFormField(formId, "df82a9bc21") }}" required autofocus> +        </div> + +        <div class="form-group" id="group2"> +            <label for="i_display_name">{{ __("Display name:") }}</label> +            <input class="form-control" id="i_display_name" type="text" name="display_name" value="{{ lastFormField(formId, "display_name") }}" required> +        </div> + +        <div class="form-group" id="group3"> +            <label for="i_password">{{ __("Choose password:") }}</label> +            <input class="form-control" id="i_password" type="password" name="password" required> +        </div> + +        <div class="form-group" id="group4"> +            <label for="i_password_retype">{{ __("Repeat password:") }}</label> +            <input class="form-control" id="i_password_retype" type="password" name="password_retype" required> +        </div> + +        <div class="form-group" id="group5"> +            <label for="i_email">{{ __("Email address:") }}</label> +            <input class="form-control" id="i_email" type="email" name="email" value="{{ lastFormField(formId, "email") }}" required> +        </div> + +        <div class="form-group" id="group6"> +            <label for="i_email">{{ __("CAPTCHA:") }}</label> +            <div class="text-center"> +                <img src="?_action=captcha&t={{ "now"|date("Uv") }}" alt="CAPTCHA" width="192" height="48" id="captcha-img"> +            </div> +            <div class="spring-row"> +                <div class="spring-fill"> +                    <input type="text" name="captcha" id="i_captcha" class="form-control" required> +                </div> +                <div class="spring-fit"> +                    <button class="btn btn-iconic" type="button" id="btn-refresh-captcha" title="{{ __("New CAPTCHA") }}"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg> +                    </button> +                </div> +            </div> +        </div> + +        <div class="form-group form-actions"> +            <button class="btn btn-primary" type="submit">{{ __("Register now") }}</button> +        </div> + +        <div class="form-group form-additional"> +            {{ __("Already have an account? %link%Sign in now%/link%", { +                "link": '<a href="?_action=auth">', +                "/link": '</a>', +            }) }} +        </div> +    </form> +</div> + +<script> +document.addEventListener("DOMContentLoaded", function() { +    document.querySelector("#btn-refresh-captcha").addEventListener("click", function() { +        document.querySelector("#captcha-img").src = "?_action=captcha&t=" + new Date().getTime().toString(); +    }); +    (function($$) { +        $$.disabled = true; +        $$.required = false; +    })(document.querySelector("#i_username")); +}); +</script> + +{% endblock %} diff --git a/src/application/templates/modern/search.twig b/src/application/templates/modern/search.twig new file mode 100644 index 0000000..3546f41 --- /dev/null +++ b/src/application/templates/modern/search.twig @@ -0,0 +1,69 @@ +{% set title = __("Search") %} +{% set formId = "search" %} +{% set formError = getAndClearFormError(formId) %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header"> +    <h1>{{ __("Search") }}</h1> +</div> + +{% if formError %} +    {% include "components/alert_error.twig" with { message: formError } %} +{% endif %} + +<form action="." method="get"> +    <input type="hidden" name="form_id" value="{{ formId }}"> +    <input type="hidden" name="_action" value="search"> +    <div class="spring-row"> +        <div class="spring-fill"> +            <input type="search" id="i_query" name="query" value="{{ lastFormField(formId, "query")|default(g.get.query)|default("") }}" required autofocus placeholder="{{ __("Enter your search query...") }}"> +        </div> +        <div class="spring-fit"> +            <button class="btn btn-primary" type="submit">{{ __("Search") }}</button> +        </div> +    </div> +</form> + +{% if g.get.query is defined and g.get.query is not null and g.get.query != "" %} +    {% if ctx.posts|length > 0 %} +        <p>{{ __("%result_count% result(s) in %search_duration% second(s)", { +            "result_count": ctx.posts|length, +            "search_duration": ctx.search_duration|number_format(2, __(".", context: "Number formatting"), __(",", context: "Number formatting")), +        }) }}</p> +        <div class="post-list"> +            {% for post in ctx.posts|filter(p => not p.deleted) %} +                {% set hasAttachments = ctx.attachments[post.id]|length > 0 %} +                {% set postAuthor = ctx.users[post.authorId] %} +                <a href="?_action=viewtopic&topic={{ post.topicId|url_encode }}#post-{{ post.id|url_encode }}" class="_item"> +                    <h4 class="_heading"> +                        {% if hasAttachments %} +                            <span class="badge"> +                                <svg viewBox="0 0 24 24" class="icon"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg> +                            </span> +                        {% endif %} +                        {{ renderPostSummary(post.content) }}<br> +                    </h4> +                    <span class="text-muted _text">{{ __("posted by %author% on %post_date% in %topic%", { +                        "author": '<em>' ~ (postAuthor ? postAuthor.displayName : __("unknown"))|e("html") ~ '</em>', +                        "post_date": '<span class="_time">' ~ post.postDate.format("c")|e("html") ~ '</span>', +                        "topic": '<em>' +                            ~ (ctx.topics[post.topicId].isLocked ? '<svg viewBox="0 0 24 24" class="icon text-muted"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> ' : '') +                            ~ (ctx.topics[post.topicId] ? ctx.topics[post.topicId].title : null)|default(__("unknown"))|e("html") ~ '</em>', +                    }) }}</span> +                </a> +            {% endfor %} +        </div> +    {% else %} +        <div class="result-alert"> +            <div class="alert alert-info"> +                <svg viewBox="0 0 24 24" class="icon"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> +                <span>{{ __("No results for this search") }}</span> +            </div> +        </div> +    {% endif %} +{% endif %} + +{% endblock %} diff --git a/src/application/templates/modern/view_topic.twig b/src/application/templates/modern/view_topic.twig new file mode 100644 index 0000000..733ce1b --- /dev/null +++ b/src/application/templates/modern/view_topic.twig @@ -0,0 +1,367 @@ +{% set canReply = +    not ctx.topic.isLocked +    and currentUser is not null +    and currentUser.hasPermission(permission("CREATE_OWN_POST")) %} + +{% set canEdit = +    currentUser is not null and ( +        ( +            ctx.topicAuthor is not null +            and currentUser.id == ctx.topicAuthor.id +            and ctx.topicAuthor.hasPermission(permission("EDIT_OWN_TOPIC")) +        ) +        or currentUser.hasPermission(permission("EDIT_OTHER_TOPIC")) +    ) %} + +{% set couldEditPost = +    currentUser is not null +    and ( +        currentUser.hasPermission(permission("EDIT_OWN_POST")) +        or currentUser.hasPermission(permission("EDIT_OTHER_POST")) +    ) %} + +{% set canDelete = +    currentUser is not null and ( +        ( +            ctx.topicAuthor is not null +            and currentUser.id == ctx.topicAuthor.id +            and ctx.topicAuthor.hasPermission(permission("DELETE_OWN_TOPIC")) +        ) +        or currentUser.hasPermission(permission("DELETE_OTHER_TOPIC")) +    ) %} + +{% set title = ctx.topic.title %} +{% extends "base.twig" %} + +{% block content %} + +{% if couldEditPost %} +    <div class="modal fade" tabindex="-1" role="dialog" id="diag-edit-post"> +        <form class="modal-dialog" role="document" action="?_action=updatepost" method="post"> +            <input type="hidden" id="i_edit_post" name="post"> +            <div class="modal-content"> +                <div class="modal-header"> +                    <button type="button" class="btn-danger close btn-iconic" data-dismiss="modal" aria-label="Close"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> +                    </button> +                    <h4 class="modal-title">{{ __("Edit post") }}</h4> +                </div> +                <div class="modal-body edit-post-wrapper"> +                    <label class="sr-only" for="i_edit_message">{{ __("Message:") }}</label> +                    {% include "components/richtext_editor.twig" with { id: "i_edit_message", name: "message" } %} +                </div> +                <div class="modal-footer"> +                    <button type="button" data-dismiss="modal"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> +                        <span>{{ __("Cancel") }}</span> +                    </button> +                    <button type="submit" class="btn btn-success"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg> +                        <span>{{ __("Save changes") }}</span> +                    </button> +                </div> +            </div> +        </form> +    </div> +{% endif %} +{% if currentUser is null %} +    <div class="modal fade" tabindex="-1" role="dialog" id="diag-cant-view-attachment"> +        <div class="modal-dialog modal-danger" role="document"> +            <div class="modal-content"> +                <div class="modal-header"> +                    <h4 class="modal-title"><svg viewBox="0 0 24 24" class="icon icon-in-text"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg> {{ __("Permission denied") }}</h4> +                </div> +                <div class="modal-body"> +                    {{ __("You must be logged in to view attachments") }} +                </div> +                <div class="modal-footer"> +                    <button type="button" data-dismiss="modal"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> +                        <span>{{ __("Close") }}</span> +                    </button> +                    <a href="?_action=auth&next={{ g.server.REQUEST_URI|url_encode }}" class="btn btn-success"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> +                        <span>{{ __("Log in") }}</span> +                    </a> +                </div> +            </div> +        </div> +    </div> +    <script> +        document.addEventListener("DOMContentLoaded", function() { +            document.querySelectorAll(".attachment").forEach(e => e.addEventListener("click", function(ev) { +                ev.preventDefault(); +                document.querySelector("#diag-cant-view-attachment").classList.remove("fade"); +            })); +        }); +    </script> +{% else %} +    <div class="modal fade" tabindex="-1" role="dialog" id="diag-image-attachment"> +        <div class="modal-dialog" role="document"> +            <div class="modal-content"> +                <div class="modal-header"> +                    <button type="button" class="btn-danger close btn-iconic" data-dismiss="modal" aria-label="Close"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> +                    </button> +                    <h4 class="modal-title">{{ __("Attachment") }}</h4> +                </div> +                <div class="modal-body"> +                    <img class="image-attachment-view attachment-view" id="image-attachment-view" alt=""> +                </div> +                <div class="modal-footer"> +                    <a href="" download id="image-attachment-dl-btn" class="btn"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg> +                        <span>{{ __("Download") }}</span> +                    </a> +                </div> +            </div> +        </div> +    </div> +    <div class="modal fade" tabindex="-1" role="dialog" id="diag-video-attachment"> +        <div class="modal-dialog" role="document"> +            <div class="modal-content"> +                <div class="modal-header"> +                    <button type="button" class="btn-danger close btn-iconic" data-dismiss="modal" aria-label="Close"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> +                    </button> +                    <h4 class="modal-title">{{ __("Attachment") }}</h4> +                </div> +                <div class="modal-body"> +                    <video class="video-attachment-view attachment-view" id="video-attachment-view" controls></video> +                </div> +                <div class="modal-footer"> +                    <a href="" download id="video-attachment-dl-btn" class="btn"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg> +                        <span>{{ __("Download") }}</span> +                    </a> +                </div> +            </div> +        </div> +    </div> +    <script> +        document.addEventListener("DOMContentLoaded", function() { +            document.querySelectorAll(".image-attachment:not(.video-attachment)").forEach(e => e.addEventListener("click", function(ev) { +                ev.preventDefault(); +                const attUrl = "?_action=attachment&attachment=" + encodeURIComponent(e.dataset.attachmentId); +                document.querySelector("#image-attachment-view").src = attUrl; +                document.querySelector("#image-attachment-dl-btn").href = attUrl; +                document.querySelector("#diag-image-attachment").classList.remove("fade"); +            })); +            document.querySelectorAll(".image-attachment.video-attachment").forEach(e => e.addEventListener("click", function(ev) { +                ev.preventDefault(); +                const attUrl = "?_action=attachment&attachment=" + encodeURIComponent(e.dataset.attachmentId); +                document.querySelector("#video-attachment-view").src = attUrl; +                document.querySelector("#video-attachment-dl-btn").href = attUrl; +                document.querySelector("#diag-video-attachment").classList.remove("fade"); +            })); +            document.querySelector("#diag-video-attachment").addEventListener("hidemodal", function() { +                document.querySelector("#video-attachment-view").pause(); +            }); +        }); +    </script> +{% endif %} + +{% set formError = getAndClearFormError("updateTopic") %} +{% if formError %} +    {% include "components/alert_error.twig" with { message: formError } %} +{% endif %} +{% set formError = getAndClearFormError("lockTopic") %} +{% if formError %} +    {% include "components/alert_error.twig" with { message: formError } %} +{% endif %} +{% set formError = null %} + +<div class="page-header"> +    <div id="displayHeading"> +        <div role="heading" class="h1 seamless-inline"> +            {% if ctx.topic.isLocked %} +                <svg viewBox="0 0 24 24" class="icon text-muted"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> +            {% endif %} +            {{ ctx.topic.title }} +            <div class="title-controls pull-right text-normal"> +                {% if canEdit and not ctx.topic.isLocked %} +                    <button id="btn-edit-title" class="btn js-only btn-iconic" title="{{ __("Edit title") }}"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg> +                    </button> +                {% endif %} +                {% if canReply %} +                    <button id="btn-reply" class="btn js-only btn-iconic" title="{{ __("Reply") }}"> +                        <svg viewBox="0 0 24 24" class="icon"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg> +                    </button> +                {% endif %} +                {% if canEdit %} +                    {% if ctx.topic.isLocked %} +                        <form action="?_action=locktopic" method="post"> +                            <input type="hidden" name="topic" value="{{ ctx.topic.id }}"> +                            <input type="hidden" name="locked" value="false"> +                            <button type="submit" class="btn btn-success btn-iconic" title="{{ __("Unlock topic") }}"> +                                <svg viewBox="0 0 24 24" class="icon"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg> +                            </button> +                        </form> +                    {% else %} +                        <form action="?_action=locktopic" method="post"> +                            <input type="hidden" name="topic" value="{{ ctx.topic.id }}"> +                            <input type="hidden" name="locked" value="true"> +                            <button type="submit" class="btn btn-warning btn-iconic" title="{{ __("Lock topic") }}"> +                                <svg viewBox="0 0 24 24" class="icon"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> +                            </button> +                        </form> +                    {% endif %} +                {% endif %} +                {% if canDelete %} +                    <form action="?_action=deletetopic" method="post"> +                        <input type="hidden" name="topic" value="{{ ctx.topic.id }}"> +                        <button type="submit" class="btn btn-danger btn-iconic" title="{{ __("Delete topic") }}"> +                            <svg viewBox="0 0 24 24" class="icon"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg> +                        </button> +                    </form> +                {% endif %} +            </div> +        </div> +        {{ __("Started by %user% on %date%", { +            "user": (ctx.topicAuthor is not null) ? ('<a href="?_action=viewuser&user=' ~ ctx.topicAuthor.id|url_encode|e("html") ~ '">' ~ ctx.topicAuthor.displayName|e("html") ~ '</a>') : __("(deleted)"), +            "date": '<span class="_time">' ~ ctx.topic.creationDate.format("c")|e("html") ~ '</span>', +        }) }} +    </div> +    {% if canEdit %} +        <form action="?_action=updatetopic" method="post" id="editHeading" hidden> +            <input type="hidden" name="topic" value="{{ ctx.topic.id }}"> +            <div class="spring-row"> +                <div class="spring-fill h1"> +                    <input type="text" name="title" id="i_edit_title" data-original-value="{{ ctx.topic.title }}" value="{{ ctx.topic.title }}"> +                </div> +                <div class="spring-fit controls"> +                    <button type="button" class="btn-iconic" id="topicTitleEditCancel" title="{{ __("Cancel") }}"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> +                    </button> +                    <button type="submit" class="btn-iconic btn-success" title="{{ __("Save changes") }}"> +                        <svg viewBox="0 0 24 24" class="icon"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg> +                    </button> +                </div> +            </div> +        </form> +    {% endif %} +</div> +{% if canEdit %} +<script> +document.addEventListener("DOMContentLoaded", function() { +    document.querySelector("#btn-edit-title").addEventListener("click", function() { +        document.querySelector("#displayHeading").hidden = true; +        document.querySelector("#editHeading").hidden = false; +        const $title = document.querySelector("#i_edit_title") +        $title.value = $title.dataset.originalValue; +        $title.focus(); +        $title.selectionStart = $title.selectionEnd = $title.value.length; +    }); +    document.querySelector("#topicTitleEditCancel").addEventListener("click", function() { +        document.querySelector("#displayHeading").hidden = false; +        document.querySelector("#editHeading").hidden = true; +    }); +}); +</script> +{% endif %} +{% if couldEditPost %} +<script> +document.addEventListener("DOMContentLoaded", function() { +    document.querySelectorAll("._edit-post").forEach(e => e.addEventListener("click", function() { +        const $post = document.querySelector("[data-post-id='" + e.dataset.postId + "']"); +        const $edit = document.querySelector("#i_edit_message"); +        $edit.style.removeProperty("height"); +        $edit.value = $post.dataset.text; +        document.querySelector("#i_edit_post").value = e.dataset.postId; +        document.querySelector("#diag-edit-post").classList.remove("fade"); +    })); +}); +</script> +{% endif %} +<script> +{% if canReply %} +document.addEventListener("DOMContentLoaded", function() { +    function focusReplyBox() { +        const msgInput = document.querySelector("#i_message"); +        msgInput.scrollIntoView(); +        msgInput.focus(); +    } +    document.querySelector("#btn-reply").addEventListener("click", function() { +        focusReplyBox(); +    }); +    document.querySelectorAll("._reply-post").forEach(e => e.addEventListener("click", function() { +        const text = document.querySelector("#post-" + e.dataset.postId).dataset.text; +        const lines = text.split("\n"); +        let val = document.querySelector("#i_message").value; +        for (let i = 0; i < lines.length; ++i) +            val += "\n> " + lines[i]; +        val += "\n\n"; +        document.querySelector("#i_message").value = val.replace(/^\n+/, ""); +        focusReplyBox(); +    })); +}); +{% endif %} +</script> + +{% for item in ctx.allItems %} +    {% if item.type == "post" %} +        {% include "components/post.twig" with { +            post: item.post, +            postAuthor: item.postAuthor, +            topicAuthor: item.topicAuthor, +            topic: item.topic, +            attachments: item.attachments, +            hide_actions: false, +            hide_pfp: false, +        } %} +    {% elseif item.type == "logMessage" %} +        {% include "components/topic_log.twig" with { +            type: item.type, +            logMessage: item.logMessage, +            postAuthor: item.postAuthor, +            topicAuthor: item.topicAuthor, +            topic: item.topic, +            hide_actions: false, +            hide_pfp: false, +        } %} +    {% endif %} +{% endfor %} + +{% if ctx.topic.isLocked %} +    <div class="topic-info-box warning topic-locked"> +        <svg viewBox="0 0 24 24" class="icon"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> +        <em>{{ __("This topic has been locked") }}</em> +    </div> +{% elseif currentUser is not null %} +    {% set formId = "addpost" %} +    <h3 id="form">{{ __("Reply to this topic") }}</h3> +    {% set formError = getAndClearFormError(formId) %} +    {% if formError %} +        {% include "components/alert_error.twig" with { message: formError } %} +    {% endif %} +    <form action="{{ g.server.REQUEST_URI }}#form" method="post" enctype="multipart/form-data" class="post-reply"> +        <input type="hidden" name="form_id" value="{{ formId }}"> +        <div class="form-group"> +            <label for="i_message" class="sr-only">{{ __("Message:") }}</label> +            {% include "components/richtext_editor.twig" with { id: "i_message", name: "message" } %} +        </div> +        <div class="form-group"> +            <label for="i_files">{{ __("Attachments: <small>(max. %max_attachment_count% files, max. %max_attachment_size% MiB each)</small>", { +                "max_attachment_count": constant("MAX_ATTACHMENT_COUNT"), +                "max_attachment_size": constant("MAX_ATTACHMENT_SIZE") // (2**20), +            }) }}</label> +            <input type="file" name="files[]" id="i_files" multiple accept="*/*"> +        </div> +        <button type="submit" class="btn btn-success"> +            <span>{{ __("Post reply") }}</span> +            <svg viewBox="0 0 24 24" class="icon"><path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/></svg> +        </button> +    </form> +{% else %} +    <div class="topic-info-box success"> +        <h3>{{ __("Log in to reply to this topic") }}</h3> +        <a href="?_action=auth&next={{ g.server.REQUEST_URI|url_encode }}" class="btn btn-success"> +            <svg viewBox="0 0 24 24" class="icon"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> +            <span>{{ __("Log in") }}</span> +        </a> +    </div> +{% endif %} + + +{% endblock %} diff --git a/src/application/templates/modern/view_topics.twig b/src/application/templates/modern/view_topics.twig new file mode 100644 index 0000000..1f24863 --- /dev/null +++ b/src/application/templates/modern/view_topics.twig @@ -0,0 +1,28 @@ +{% extends "base.twig" %} + +{% block content %} + +{% if currentUser.hasPermission(permission("CREATE_OWN_TOPIC")) %} +    <div class="actions"> +        <a href="?_action=newtopic" class="btn"> +            <svg viewBox="0 0 24 24" class="icon"><path d="m5 19-2 2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2"/><path d="M9 10h6"/><path d="M12 7v6"/><path d="M9 17h6"/></svg> +            <span>{{ __("New topic") }}</span> +        </a> +    </div> +{% endif %} + +<div class="post-list"> +    {% for topic in ctx.topics %} +        <a class="_item" href="?_action=viewtopic&topic={{ topic.id|url_encode }}"> +            <h4 class="_heading"> +                {% if topic.isLocked %} +                    <svg viewBox="0 0 24 24" class="icon text-muted"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> +                {% endif %} +                {{ topic.title }} +            </h4> +            <p class="_text _time">{{ topic.creationDate.format("c") }}</p> +        </a> +    {% endfor %} +</div> + +{% endblock %} diff --git a/src/application/templates/modern/view_user.twig b/src/application/templates/modern/view_user.twig new file mode 100644 index 0000000..f2b8440 --- /dev/null +++ b/src/application/templates/modern/view_user.twig @@ -0,0 +1,196 @@ +{% set canEdit = +    currentUser is not null +    and ( +        ( +            ctx.user.id == currentUser.id +            and currentUser.hasPermission(permission("EDIT_OWN_USER")) +        ) +        or currentUser.hasPermission(permission("EDIT_OTHER_USER")) +    ) %} + +{% set isOwnProfile = +    currentUser is not null +    and currentUser.id == ctx.user.id %} + +{% set sUserPossessive = isOwnProfile ? "Your posts" : "%display_name%'s posts" %} + +{% set emailPending = isOwnProfile and ctx.user.pendingEmail is not null %} + +{% set title = ctx.user.displayName %} + +{% extends "base.twig" %} + +{% block content %} + +<div class="page-header profile-header"> +    <img class="_image" src="?_action=profilepicture&user={{ ctx.user.id|url_encode }}" alt="{{ __("Profile picture") }}" width="64" height="64"> +    <div class="h1 _name"> +        {{ ctx.user.displayName }} +        {% if isOwnProfile %} +            <svg viewBox="0 0 24 24" class="icon is-you icon-in-text"><title>{{ __("You") }}</title><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> +        {% endif %} +    </div> +    <span class="_info"> +        @{{ ctx.user.name }} • <span class="text-muted">{{ __("Member since %join_date%", { +            "join_date": '<span class="_date">' ~ (ctx.dateJoined.format("c")|e("html")) ~ '</span>', +        }) }}</span> +    </span> +</div> + +{% if canEdit %} +<div class="profile-edit-view"> +<div class="_posts"> +{% endif %} + +<h3>{{ __(sUserPossessive, { +    "display_name": ctx.user.displayName|e("html"), +}) }}</h3> + +{% if ctx.posts|length > 0 %} +    <div class="post-list"> +        {% for post in ctx.posts %} +            <a href="?_action=viewtopic&topic={{ post.topicId|url_encode }}#post-{{ post.id|url_encode }}" class="_item"> +                <h4 class="_heading"> +                    {% if hasAttachments %} +                        <span class="badge"> +                            <svg viewBox="0 0 24 24" class="icon"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg> +                        </span> +                    {% endif %} +                    {{ renderPostSummary(post.content) }}<br> +                </h4> +                <span class="text-muted _text">{{ __("posted on %post_date% in %topic%", { +                    "post_date": '<span class="_time">' ~ post.postDate.format("c")|e("html") ~ '</span>', +                    "topic": '<em>' ~ +                        (ctx.topics[post.topicId] is not null and ctx.topics[post.topicId].isLocked ? '<svg viewBox="0 0 24 24" class="icon text-muted"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> ' : '') ~ +                        (ctx.topics[post.topicId] is not null ? ctx.topics[post.topicId].title : __("unknown"))|e("html") ~ '</em>', +                }) }}</span> +            </a> +        {% endfor %} +    </div> +{% else %} +    <div class="result-alert"> +        <div class="alert alert-info"> +            <svg viewBox="0 0 24 24" class="icon"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> +            <span>{{ __("This user has not posted anything yet") }}</span> +        </div> +    </div> +{% endif %} + +{% if canEdit %} +</div> <!-- _posts --> + +<div class="_edit"> +    <h3>{{ __("Edit profile") }}</h3> +    {% set formId = "update_profile" %} +    {% set formError = getAndClearFormError(formId) %} +    {% if formError %} +        {% include "components/alert_error.twig" with { message: formError } %} +    {% endif %} +    <form action="{{ g.server.REQUEST_URI }}" method="post" enctype="multipart/form-data"> +        <input type="hidden" name="form_id" value="{{ formId }}"> +        <div class="form-group"> +            <label for="i_display_name">{{ __("Display name:") }}</label> +            <input required class="form-control" type="text" name="display_name" id="i_display_name" value="{{ ctx.user.displayName }}"> +        </div> +        <div class="form-group"> +            <label for="i_name">{{ __("Username:") }}</label> +            {% if ctx.lastNameChangeTooRecent %} +                <input class="form-control" type="text" id="i_name" value="{{ ctx.user.name }}" disabled> +                <small class="text-danger"><strong>{{ __("You can only change your username every 30 days!") }}</strong></small> +            {% else %} +                <input required class="form-control" type="text" name="name" id="i_name" value="{{ ctx.user.name }}"> +            {% endif %} +        </div> +        <div class="form-group"> +            <label for="i_email">{{ __("Email address:") }}</label> +            {% if emailPending %} +                <input class="form-control" type="email" id="i_email" value="{{ ctx.user.email }}" disabled> +            {% else %} +                <input required class="form-control" type="email" id="i_email" name="email" value="{{ ctx.user.email }}"> +            {% endif %} +        </div> +        <div class="form-group"> +            <label>{{ __("Profile picture:") }}</label> +            <div class="radio {{ ctx.user.profilePicture is empty ? " disabled text-muted" }}"> +                <label> +                    <input type="radio" name="pfp_action" id="pfp_action_1" value="keep"{{ ctx.user.profilePicture is not empty ? ' checked' : ' disabled' }}> +                    {{ __("Keep current profile picture") }} +                </label> +            </div> +            <div class="radio"> +                <label> +                    <input type="radio" name="pfp_action" id="pfp_action_2" value="remove"{{ ctx.user.profilePicture is empty ? ' checked' : '' }}> +                    {% if ctx.user.profilePicture is empty %} +                        {{ __("No profile picture") }} +                    {% else %} +                        {{ __("Remove profile picture") }} +                    {% endif %} +                </label> +            </div> +            <div class="radio"> +                <label> +                    <input type="radio" name="pfp_action" value="replace" id="pfp_action_3"> +                    {{ __("Upload new profile picture") }} +                </label> +            </div> +            <input type="file" name="pfp" id="i_pfp" accept="image/png,image/jpeg"> +        </div> +        <div class="form-group form-actions"> +            <button type="submit" class="btn btn-primary">{{ __("Save changes") }}</button> +        </div> +    </form> +    {% if isOwnProfile %} +        <h3>{{ __("Change password") }}</h3> +        {% set formId = "update_password" %} +        {% set formError = getAndClearFormError(formId) %} +        {% if formError %} +            {% include "components/alert_error.twig" with { message: formError } %} +        {% endif %} +        <form action="{{ g.server.REQUEST_URI }}" method="post"> +            <input type="hidden" name="form_id" value="{{ formId }}"> +            <div class="form-group"> +                <label for="i_current_password">{{ __("Current password:") }}</label> +                <input autocomplete="current-password" required class="form-control" type="password" name="current_password" id="i_current_password" required> +            </div> +            <div class="form-group"> +                <label for="i_new_password">{{ __("New password:") }}</label> +                <input autocomplete="new-password" required class="form-control" type="password" name="new_password" id="i_new_password" required> +            </div> +            <div class="form-group"> +                <label for="i_retype_password">{{ __("Retype password:") }}</label> +                <input autocomplete="new-password" required class="form-control" type="password" name="retype_password" id="i_retype_password" required> +            </div> +            <div class="form-group form-actions"> +                <button type="submit" class="btn btn-primary">{{ __("Change password") }}</button> +            </div> +        </form> +    {% endif %} +</div> <!-- ._edit --> +</div> <!-- .profile-edit-view --> +{% endif %} + +{% if canEdit %} +<script> +document.addEventListener("DOMContentLoaded", function() { +    const $i_pfp = document.querySelector("#i_pfp"); +    function _hide() { +        $i_pfp.hidden = true; +        $i_pfp.disabled = true; +        $i_pfp.required = false; +    } +    _hide(); +    setTimeout(_hide, 10); +    document.querySelectorAll("[name='pfp_action']").forEach(e => "change input check click".split(" ").forEach(n => e.addEventListener(n, () => { +        if (document.querySelector("#pfp_action_3").checked) { +            $i_pfp.hidden = false; +            $i_pfp.disabled = false; +            $i_pfp.required = true; +        } else { +            _hide(); +        } +    }))); +}); +</script> +{% endif %} + +{% endblock %} diff --git a/src/application/themes/modern/theme.json b/src/application/themes/modern/theme.json new file mode 100644 index 0000000..f545766 --- /dev/null +++ b/src/application/themes/modern/theme.json @@ -0,0 +1,11 @@ +{ +    "$format": 1, +    "version": "1.0.0", +    "id": "modern", +    "name": "Modern", +    "author": "Jonas Kohl", +    "template": "modern", +    "files": [ +        "../../../ui/theme-files/modern/theme.css" +    ] +} diff --git a/src/ui/theme-files/modern/InterVariable-Italic.woff2 b/src/ui/theme-files/modern/InterVariable-Italic.woff2Binary files differ new file mode 100644 index 0000000..f22ec25 --- /dev/null +++ b/src/ui/theme-files/modern/InterVariable-Italic.woff2 diff --git a/src/ui/theme-files/modern/InterVariable.woff2 b/src/ui/theme-files/modern/InterVariable.woff2Binary files differ new file mode 100644 index 0000000..22a12b0 --- /dev/null +++ b/src/ui/theme-files/modern/InterVariable.woff2 diff --git a/src/ui/theme-files/modern/theme.css b/src/ui/theme-files/modern/theme.css new file mode 100644 index 0000000..094e7fc --- /dev/null +++ b/src/ui/theme-files/modern/theme.css @@ -0,0 +1,970 @@ +@font-face { +    font-family: Inter; +    font-style: normal; +    font-weight: 100 900; +    font-display: swap; +    src: url('/ui/theme-files/modern/InterVariable.woff2?v=4.0') format('woff2'); +} +@font-face { +    font-family: Inter; +    font-style: italic; +    font-weight: 100 900; +    font-display: swap; +    src: url('/ui/theme-files/modern/InterVariable-Italic.woff2?v=4.0') format('woff2'); +} + +*, *::before, *::after { box-sizing: border-box; } +:root { +    color-scheme: light dark; +    scroll-behavior: smooth; +    accent-color: var(--color--primary); + +    --header-height: 52px; +    --border-radius: 0; + +    --font-family--sans-serif: 'Inter', ui-sans-serif, system-ui, sans-serif; +    --font-family--monospace: ui-monospace, monospace; +    --font-size--base: 11pt; + +    --color--primary: #07d; +    --color--info: #0ac; +    --color--success: #0c8; +    --color--warning: #d90; +    --color--danger: #c08; +    --color--muted: #707d8a; + +    --color--background: #fff; +    --color--text: #000; +    --color--links: var(--color--primary); +     +    --color--btn--default-bg: #000; +    --color--btn--default-fg: #fff; + +    --color--btn--primary-bg: var(--color--primary); +    --color--btn--primary-fg: #fff; + +    --color--btn--info-bg: var(--color--info); +    --color--btn--info-fg: #000; + +    --color--btn--success-bg: var(--color--success); +    --color--btn--success-fg: #000; + +    --color--btn--warning-bg: var(--color--warning); +    --color--btn--warning-fg: #000; + +    --color--btn--danger-bg: var(--color--danger); +    --color--btn--danger-fg: #fff; + +    --widget--border-width: 1px; +    --widget--border-color: #bbb; +} + +@media (prefers-color-scheme: dark) { +    :root { +        --color--primary: #9cf; +        --color--info: #9ef; +        --color--success: #9fa; +        --color--warning: #fd9; +        --color--danger: #f9b; +        --color--muted: #97a3af; + +        --color--background: #0b0b0b; +        --color--text: #e1e1e1; +         +        --color--btn--default-bg: #fff; +        --color--btn--default-fg: #000; + +        --color--btn--primary-fg: #000; +        --color--btn--danger-fg: #000; + +        --widget--border-color: #3b3b3b; + +        select { +            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23e1e1e1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); +        } +    } +} + +html { +    height: 100%; +} + +body { +    min-height: 100%; + +    background: var(--color--background); +    color: var(--color--text); + +    display: grid; +    max-inline-size: 1000px; +    margin-inline: auto; +    margin-block: 0; +    grid-template: "header" max-content +                   "main" minmax(0, 1fr) +                   "footer" max-content +                 / minmax(0, 1fr); +    font: var(--font-size--base) var(--font-family--sans-serif); +    font-optical-sizing: auto; +    box-shadow: calc(var(--widget--border-width) * -1) 0 0 var(--widget--border-color), +                var(--widget--border-width) 0 0 var(--widget--border-color); +} + +[hidden] { +    display: none !important; +} + +h1, +.h1 { +    font-size: 2em; +    font-weight: 200; +} + +a { +    color: var(--color--links); +} + +:focus-visible { +    outline: 1px solid var(--_focus-color, var(--_bg, var(--color--primary))); +    outline-offset: 2px; +} + +header { +    grid-area: header; +    display: grid; +    grid-template: "brand nav" minmax(0, 1fr) +                 / max-content minmax(0, 1fr); +    /* padding: 16px 8px; */ +    border-bottom: var(--widget--border-width) solid var(--widget--border-color); +    position: sticky; +    top: 0; +    background: var(--color--background); +    z-index: 3; +    height: var(--header-height); +    align-items: stretch; +} +#brand { +    display: block; +    grid-area: brand; +    align-content: center; +    margin-left: 16px; +    font-weight: 400; +    text-decoration: none; +    color: inherit; +} +nav { +    display: flex; +    justify-content: end; +    align-items: stretch; +    grid-area: nav; +    padding-right: 8px; +    >p { +        margin: 0; +        align-content: center; +    } +    >a { +        padding: 16px 8px; +        color: inherit; +        text-decoration: none; +        align-content: center; +        &:hover { +            background-color: rgb(from var(--color--text) r g b / 10%); +        } +        >svg { +            display: block; +        } +        &.active { +            box-shadow: calc(var(--widget--border-width) * -1) 0 0 var(--widget--border-color), +                        var(--widget--border-width) 0 0 var(--widget--border-color); +        } +    } +    >* { +        display: block; +    } +} +main { +    grid-area: main; +    padding: 8px; +} +footer { +    grid-area: footer; +    display: grid; +    grid-template: "about prefs" minmax(0, 1fr) +                 / minmax(0, 1fr) max-content; +    align-items: center; +    padding: 16px; +    border-top: var(--widget--border-width) solid var(--widget--border-color); +    #about { +        grid-area: about; +    } +    #preferences { +        grid-area: prefs; +    } +} +.form-group { +    display: flex; +    flex-direction: column; +    margin-block: 8px; +    label { +        /* font-size: 80%; +        font-weight: bold; */ +        align-self: start; +    } +    >input, >select, >textarea, >.input { +        align-self: stretch; +        width: 100%; +        display: block; +    } +    &.form-actions { +        flex-direction: row; +        justify-content: start; +        align-items: center; +        gap: 8px; +    } +    &.form-additional { +        display: block; +    } +} +.form-inline { +    display: inline-flex; +    margin: 0; +    padding: 0; +    align-items: center; +    gap: 8px; +    .form-group { +        margin: 0; +        flex-direction: row; +        align-items: center; +        label { +            margin-right: 0.25em; +        } +        >* { +            align-self: unset; +        } +    } +} + +.main-form { +    max-inline-size: 480px; +    margin-inline: auto; +    margin-top: 32px; +} + +input:is( +    :not([type]), +    [type="text"], +    [type="email"], +    [type="password"], +    [type="search"], +    [type="number"] +), +textarea, +select { +    appearance: none; +    border: var(--widget--border-width) solid var(--widget--border-color); +    background: none; +    font: inherit; +    border-radius: var(--border-radius); +    padding: 6px; + +    &:focus-visible { +        background-color: rgb(from var(--color--primary) r g b / 10%); +    } +} + +select { +    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); +    background-size: 16px 16px; +    background-repeat: no-repeat; +    background-position: right 4px center; +    padding-right: 24px; +} + +button, +input:is( +    [type="button"], +    [type="submit"], +    [type="reset"] +), +.btn { +    appearance: none; +    background: none; +    display: inline-flex; +    align-items: center; +    font-family: var(--font-family--sans-serif); +    font-weight: 800; +    font-size: var(--font-size--base); +    gap: 0.25em; +    text-decoration: none; +    padding: 6px 16px; +    border-radius: var(--border-radius); +    border: var(--widget--border-width) solid var(--widget--border-color); +    cursor: pointer; +    color: var(--color--text); + +    &:hover { +        background-color: rgb(from var(--color--text) r g b / 10%); +    } + +    &.btn-link { +        border-color: transparent; +        color: var(--color--links); + +        text-decoration: underline; +        font-weight: inherit; +    } + +    &.btn-default { +        --_bg: var(--color--btn--default-bg); +        --_fg: var(--color--btn--default-fg); +    } +    &.btn-primary { +        --_bg: var(--color--btn--primary-bg); +        --_fg: var(--color--btn--primary-fg); +    } +    &.btn-info { +        --_bg: var(--color--btn--info-bg); +        --_fg: var(--color--btn--info-fg); +    } +    &.btn-success { +        --_bg: var(--color--btn--success-bg); +        --_fg: var(--color--btn--success-fg); +    } +    &.btn-warning { +        --_bg: var(--color--btn--warning-bg); +        --_fg: var(--color--btn--warning-fg); +    } +    &.btn-danger { +        --_bg: var(--color--btn--danger-bg); +        --_fg: var(--color--btn--danger-fg); +    } + +    &:is( +        .btn-default, +        .btn-primary, +        .btn-info, +        .btn-success, +        .btn-warning, +        .btn-danger +    ) { +        box-shadow: none; +        background: var(--_bg); +        border-color: var(--_bg); +        color: var(--_fg); +        &:hover { +            border-color: hsl(from var(--_bg) h s calc(l + 15)); +            background: hsl(from var(--_bg) h s calc(l + 15)); +        } +    } + +    &.btn-iconic { +        padding-inline: 6px; +    } +} + +.icon { +    width: 16px; +    height: 16px; +    fill: none; +    stroke: currentColor; +    stroke-width: 2px; +    stroke-linecap: round; +    stroke-linejoin: round; +    flex-shrink: 0; +} + +.post-list { +    border-top: var(--widget--border-width) solid var(--widget--border-color); +    margin-inline: -8px; +    >._item { +        display: block; +        border-bottom: var(--widget--border-width) solid var(--widget--border-color); +        padding: 16px; +        color: inherit; +        text-decoration: none; +        &:hover { +            background-color: rgb(from var(--color--text) r g b / 10%); +        } +        >._heading { +            font-size: inherit; +            font-weight: bold; +            margin: 0; +        } +        >._text { +            margin: 0; +        } +    } +} + +.actions { +    inline-size: max-content; +    margin-inline-start: auto; +    margin-bottom: 8px; +    white-space: nowrap; +    &:first-child { +        margin: -8px -8px 0 auto; +        >.btn { +            border-top: 0; +            border-bottom: 0; +            border-right: 0; +        } +    } +} + +.page-actions { +    display: flex; +    justify-content: end; +    border-bottom: var(--widget--border-width) solid var(--widget--border-color); +    margin-inline: -8px; +    gap: 8px; +    button, +    .btn { +        border-top: 0; +        border-bottom: 0; +    } +    form { +        display: contents; +        margin: 0; +        padding: 0; +    } +} + +.rt-toolbar, +.btn-toolbar { +    display: flex; +    gap: 8px; +    button, +    .btn { +        border-width: 0; +        border-right-width: var(--widget--border-width); +        &:first-child { +            border-left-width: var(--widget--border-width); +        } +    } +    .rt-group, +    .btn-group { +        display: flex; +        &:first-child > button:first-child { +            border-left-width: 0; +        } +    } +} + +.btn-toolbar .btn-group { +    border-top: var(--widget--border-width) solid var(--widget--border-color); +    border-bottom: var(--widget--border-width) solid var(--widget--border-color); +} + +.text-primary { color: var(--color--primary); } +.text-info { color: var(--color--info); } +.text-success { color: var(--color--success); } +.text-warning { color: var(--color--warning); } +.text-danger { color: var(--color--danger); } +.text-muted { color: var(--color--muted); } +.text-left { text-align: left; } +.text-center { text-align: center; } +.text-right { text-align: right; } + +.richtext-editor { +    display: flex; +    flex-direction: column; +    border: var(--widget--border-width) solid var(--widget--border-color); +    .rt-toolbar { +        border-bottom: var(--widget--border-width) solid var(--widget--border-color); +    } +    textarea { +        font-family: var(--font-family--monospace); +        border: none; +        resize: vertical; +        max-height: 500px; +    } +} + +.post-reply .richtext-editor { +    margin-inline: -8px; +    border-left: 0; +    border-right: 0; +} + +.sr-only { +    display: inline-block; +    position: absolute; +    width: 0; +    height: 0; +    clip: rect(0 0 0 0); +    overflow: hidden; +    opacity: 0; +} + +.modal { +    position: fixed; +    inset: 0; +    z-index: 100; +    background: #000c; +    transition: opacity 0.2s ease; +    padding: 16px; +    &.fade { +        opacity: 0; +        pointer-events: none; +        .modal-dialog { +            translate: 0% -128px; +        } +    } +} +.modal-dialog { +    background: var(--color--background); +    color: var(--color--text); +    border: var(--widget--border-width) solid var(--widget--border-color); +    max-inline-size: 1002px; +    margin: auto; +    transition: inherit; +    transition-property: translate; +    &.modal-primary .modal-header { +        color: var(--color--primary); +        background-color: rgb(from var(--color--primary) r g b / 10%); +    } +    &.modal-info .modal-header { +        color: var(--color--info); +        background-color: rgb(from var(--color--info) r g b / 10%); +    } +    &.modal-success .modal-header { +        color: var(--color--success); +        background-color: rgb(from var(--color--success) r g b / 10%); +    } +    &.modal-warning .modal-header { +        color: var(--color--warning); +        background-color: rgb(from var(--color--warning) r g b / 10%); +    } +    &.modal-danger .modal-header { +        color: var(--color--danger); +        background-color: rgb(from var(--color--danger) r g b / 10%); +    } +} +.modal-header { +    display: grid; +    grid-template-columns: minmax(0, 1fr) max-content; +    padding: 0 16px; +    border-bottom: var(--widget--border-width) solid var(--widget--border-color); +    align-items: center; +    >button, +    >.btn { +        border-top: 0; +        border-bottom: 0; +    } +    >.close { +        margin-right: -16px; +    } +} +.modal-header>.close { +    grid-column: 2; +} +.modal-title { +    grid-column: 1; +    grid-row: 1; +    margin: 0; +    font-size: inherit; +    font-weight: 800; +    &:has(>svg) { +        display: flex; +        gap: 0.25em; +        align-items: center; +        >svg { +            display: block; +        } +    } +} +.modal-footer { +    display: flex; +    border-top: var(--widget--border-width) solid var(--widget--border-color); +    padding: 0 16px; +    gap: 8px; +    justify-content: end; +    >button, +    >.btn { +        border-top: 0; +        border-bottom: 0; +    } +} +.modal-body { +    padding: 16px; +} + +.edit-post-wrapper { +    padding: 0; +    >.richtext-editor { +        border: 0; +    } +} + +.pull-right { +    float: right; +} + +:has(>.pull-right)::after { +    content: ""; +    display: block; +    clear: both; +} + +.title-controls { +    display: flex; +    gap: 8px; +    align-items: center; +    >form { +        display: contents; +    } +} + +.page-header { +    padding-block-end: 8px; +    margin-bottom: 8px; +    margin-inline: -8px; +    padding-inline: 8px; +    border-bottom: var(--widget--border-width) solid var(--widget--border-color); +    >h1 { +        margin: 0; +    } +} +.page-header:has(~ .post) { +    margin-bottom: 0; +} + +.post { +    border-bottom: var(--widget--border-width) solid var(--widget--border-color); +    display: grid; +    grid-template-columns: max-content minmax(0, 1fr); +    margin-inline: -8px; +    z-index: 0; +    position: relative; +} +.post-pfp { +    border-right: var(--widget--border-width) solid var(--widget--border-color); +    padding: 8px; +    position: relative; +    >a { +        &, >img { +            display: block; +        } +    } +} +.post-title { +    border-bottom: var(--widget--border-width) solid var(--widget--border-color); +    padding: 8px; +    position: sticky; +    top: var(--header-height); +    background: var(--color--background); +    font-size: 80%; +} +.post-attachments { +    padding: 8px; +    border-top: var(--widget--border-width) solid var(--widget--border-color); +} +.post-main { +    padding: 8px; +} +.post-body:has(>.post-status) { +    align-content: center; +} +.post-status { +    padding: 8px; +    display: flex; +    gap: 0.25em; +    height: 100%; +    align-items: center; +    &.post-status-primary { +        color: var(--color--primary); +        background-color: rgb(from var(--color--primary) r g b / 10%); +    } +    &.post-status-info { +        color: var(--color--info); +        background-color: rgb(from var(--color--info) r g b / 10%); +    } +    &.post-status-success { +        color: var(--color--success); +        background-color: rgb(from var(--color--success) r g b / 10%); +    } +    &.post-status-warning { +        color: var(--color--warning); +        background-color: rgb(from var(--color--warning) r g b / 10%); +    } +    &.post-status-danger { +        color: var(--color--danger); +        background-color: rgb(from var(--color--danger) r g b / 10%); +    } +} +.post-content { +    word-break: break-word; +    hyphens: auto; +} + +.icon-in-text { +    vertical-align: middle; +} + +.is-you { +    color: var(--color--primary); +} + +blockquote { +    padding: 8px; +    margin-left: 0; +    border-left: var(--widget--border-width) solid var(--widget--border-color); +} + +#image-attachment-view, +#video-attachment-view { +    max-width: 100%; +    margin-inline: auto; +    display: block; +} + +.spring-row { +    display: flex; +    align-items: center; +    gap: 8px; +    >.spring-fill { +        flex: 1; +    } +    >.spring-fit { +        flex-shrink: 0; +    } +} + +.spring-fill input { +    display: block; +    width: 100%; +} + +.post-anchor { +    position: absolute; +    left: 0; +    top: calc(-1 * var(--header-height)); +    width: 0; +    height: 0; +} + +.topic-info-box { +    padding: 16px; +    text-align: center; +    margin: 0 -8px calc(-8px - var(--widget--border-width)); +    border-bottom: var(--widget--border-width) solid var(--widget--border-color); +    &.primary { +        background-color: rgb(from var(--color--primary) r g b / 10%); +    } +    &.info { +        background-color: rgb(from var(--color--info) r g b / 10%); +    } +    &.success { +        background-color: rgb(from var(--color--success) r g b / 10%); +    } +    &.warning { +        background-color: rgb(from var(--color--warning) r g b / 10%); +    } +    &.danger { +        background-color: rgb(from var(--color--danger) r g b / 10%); +    } + +    >h3 { +        margin: 0 0 8px; +    } +} + +.alert { +    padding: 16px; +    margin-bottom: 16px; +    border: var(--widget--border-width) solid var(--widget--border-color); +    &.alert-primary { +        color: var(--color--primary); +        background-color: rgb(from var(--color--primary) r g b / 10%); +    } +    &.alert-info { +        color: var(--color--info); +        background-color: rgb(from var(--color--info) r g b / 10%); +    } +    &.alert-success { +        color: var(--color--success); +        background-color: rgb(from var(--color--success) r g b / 10%); +    } +    &.alert-warning { +        color: var(--color--warning); +        background-color: rgb(from var(--color--warning) r g b / 10%); +    } +    &.alert-danger { +        color: var(--color--danger); +        background-color: rgb(from var(--color--danger) r g b / 10%); +    } +    &:is(main>.alert) { +        padding: 24px; +        margin: -8px -8px 0; +        border-left: 0; +        border-top: 0; +        border-right: 0; +        &:is(.page-header:has(~ .post) + .alert) { +            margin-top: 0; +        } +    } +    >svg { +        vertical-align: sub; +    } +} + +.result-alert { +    margin: 8px -8px 0; +    >.alert { +        padding: 24px; +        margin: 0; +        border-left: 0; +        border-right: 0; +    } +    &:is(.profile-edit-view .result-alert) { +        margin-inline: 0; +    } +} + +.topic-locked { +    display: flex; +    justify-content: center; +    align-items: center; +    text-align: center; +    gap: 0.25em; +    color: var(--color--warning); +} + +.profile-header { +    display: grid; +    grid-template: "image name" minmax(0, 1fr) +                   "image info" max-content +                 / max-content minmax(0, 1fr); +    column-gap: 8px; +    >._image { +        grid-area: image; +    } +    >._name { +        grid-area: name; +        align-self: center; +    } +    >._info { +        grid-area: info; +        align-self: center; +    } +} + +main:has(>.profile-edit-view) { +    display: flex; +    flex-direction: column; +} + +.profile-edit-view { +    margin: -8px; +    display: grid; +    grid-template: "posts edit" minmax(0, 1fr) +                 / minmax(0, 1fr) max-content; +    flex: 1; +    h3 { +        padding-inline: 8px; +    } +    >._posts { +        grid-area: posts; +        .post-list { +            margin-inline: 0; +        } +    } +    >._edit { +        grid-area: edit; +        border-left: var(--widget--border-width) solid var(--widget--border-color); +        width: 300px; +        h3 + form { +            border-top: var(--widget--border-width) solid var(--widget--border-color); +        } +        form { +            padding-inline: 8px; +        } +        form:has(+ h3) { +            border-bottom: var(--widget--border-width) solid var(--widget--border-color); +        } +    } +} + +.post-images { +    display: flex; +    gap: 8px; +    margin-top: 8px; +} + +.image-attachment { +    display: block; +    margin-right: 8px; +    text-decoration: none; +    position: relative; +    .image-attachment-image { +        display: block; +        border: var(--widget--border-width) solid var(--widget--border-color); +    } +    .attachment-lock { +        position: absolute; +        color: white; +        right: 6px; +        top: 6px; +        text-shadow: 0 1px 3px rgba(0,0,0,.5); +    } +    .video-player-icon { +        position: absolute; +        color: white; +        inset: 0; +        margin: auto; +        filter: drop-shadow(0 1px 3px rgba(0,0,0,.5)); +        width: 24px; +        height: 24px; +    } +} + +@media (max-width: 900px) { +    footer { +        display: block; +        #preferences { +            margin-top: 8px; +        } +    } +} + +@media (max-width: 780px) { +    main:has(>.profile-edit-view) { +        display: block; +    } +     +    .profile-edit-view { +        grid-template: "edit" max-content +                       "posts" minmax(0, 1fr) +                     / minmax(0, 1fr); +        >._posts { +            margin-bottom: 8px; +        } +        >._edit { +            width: auto; +            border-bottom: var(--widget--border-width) solid var(--widget--border-color); +            border-left: 0; +        } +    } +} + +@media (max-width: 560px) { +    .post { +        grid-template-columns: minmax(0, 1fr); +        .post-pfp { +            display: none; +        } +    } +    footer #preferences form { +        display: block; +        margin-top: 4px; +    } +} + +#group0 { +    display: none !important; +} + +@media (prefers-reduced-motion) { +    *, *::before, *::after { +        transition: none !important; +    } +} |