Merge branch 'master' into master2

This commit is contained in:
edshot99 2025-02-27 05:24:25 -06:00
commit 9923b01141
123 changed files with 2766 additions and 945 deletions

View File

@ -19,6 +19,12 @@ class PostsController < ApplicationController
@query = tag_query.nil? ? [] : tag_query.strip.split(/ /, 2).compact_blank @query = tag_query.nil? ? [] : tag_query.strip.split(/ /, 2).compact_blank
if @query.length == 1 if @query.length == 1
@wiki_page = WikiPage.titled(@query[0]) @wiki_page = WikiPage.titled(@query[0])
# redirect?
if @wiki_page.present? && @wiki_page.parent.present?
@wiki_page = WikiPage.titled(@wiki_page.parent)
end
@wiki_text = @wiki_page.present? ? @wiki_page.body : "" @wiki_text = @wiki_page.present? ? @wiki_page.body : ""
if @wiki_text.present? if @wiki_text.present?
@wiki_text = @wiki_text @wiki_text = @wiki_text

View File

@ -123,9 +123,10 @@ class WikiPagesController < ApplicationController
end end
def wiki_page_params(context) def wiki_page_params(context)
permitted_params = %i[body edit_reason] permitted_params = %i[body category_id edit_reason]
permitted_params += %i[parent] if CurrentUser.is_privileged? permitted_params += %i[parent] if CurrentUser.is_privileged?
permitted_params += %i[is_locked is_deleted skip_secondary_validations] if CurrentUser.is_janitor? permitted_params += %i[is_locked is_deleted skip_secondary_validations] if CurrentUser.is_janitor?
permitted_params += %i[category_is_locked] if CurrentUser.is_admin?
permitted_params += %i[title] if context == :create || CurrentUser.is_janitor? permitted_params += %i[title] if context == :create || CurrentUser.is_janitor?
params.fetch(:wiki_page, {}).permit(permitted_params) params.fetch(:wiki_page, {}).permit(permitted_params)

View File

@ -121,6 +121,10 @@ module ApplicationHelper
time_tag(time.strftime("%Y-%m-%d %H:%M"), time) time_tag(time.strftime("%Y-%m-%d %H:%M"), time)
end end
def compact_date(time)
time_tag(time.strftime("%Y-%m-%d"), time)
end
def external_link_to(url, truncate: nil, strip_scheme: false, link_options: {}) def external_link_to(url, truncate: nil, strip_scheme: false, link_options: {})
text = url text = url
text = text.gsub(%r!\Ahttps?://!i, "") if strip_scheme text = text.gsub(%r!\Ahttps?://!i, "") if strip_scheme
@ -189,21 +193,6 @@ module ApplicationHelper
end end
end end
def simple_avatar(user, **options)
return "" if user.nil?
post_id = user.avatar_id
deferred_post_ids.add(post_id) if post_id
klass = options.delete(:class)
named = options.delete(:named)
tag.a href: home_users_path, class: "simple-avatar #{klass}", data: { id: post_id, name: user.name } do
tag.span(class: "simple-avatar-button") do
concat tag.span(user.pretty_name, class: "simple-avatar-name") if named
concat tag.span(class: "simple-avatar-image", data: { name: user.name[0].capitalize })
end
end
end
def user_banner(user) def user_banner(user)
return "" if user.nil? return "" if user.nil?
post_id = user.banner_id post_id = user.banner_id

View File

@ -17,10 +17,21 @@ module IconHelper
swatch: %(<path d="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"/><path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"/><path d="M 7 17h.01"/><path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"/>), swatch: %(<path d="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"/><path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"/><path d="M 7 17h.01"/><path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"/>),
settings: %(<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>), settings: %(<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>),
log_in: %(<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/>), log_in: %(<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/>),
user: %(<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>),
# Utility # Utility
plus: %(<path d="M5 12h14"/><path d="M12 5v14"/>),
times: %(<path d="M18 6 6 18"/><path d="m6 6 12 12"/>), times: %(<path d="M18 6 6 18"/><path d="m6 6 12 12"/>),
reset: %(<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>), reset: %(<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>),
replace: %(<path d="M14 4a2 2 0 0 1 2-2"/><path d="M16 10a2 2 0 0 1-2-2"/><path d="M20 2a2 2 0 0 1 2 2"/><path d="M22 8a2 2 0 0 1-2 2"/><path d="m3 7 3 3 3-3"/><path d="M6 10V5a3 3 0 0 1 3-3h1"/><rect x="2" y="14" width="8" height="8" rx="2"/>),
upload: %(<path d="M12 13v8"/><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="m8 17 4-4 4 4"/>),
stamp: %(<path d="M19.27 13.73A2.5 2.5 0 0 0 17.5 13h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-1.5c0-.66-.26-1.3-.73-1.77Z"/><path d="M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-3-3c-1.66 0-3 1-3 3s1 2 1 3.5V13"/>),
power: %(<path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/>),
circle_help: %(<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"/>),
notepad: %(<path d="M8 2v4"/><path d="M12 2v4"/><path d="M16 2v4"/><path d="M16 4h2a2 2 0 0 1 2 2v2"/><path d="M20 12v2"/><path d="M20 18v2a2 2 0 0 1-2 2h-1"/><path d="M13 22h-2"/><path d="M7 22H6a2 2 0 0 1-2-2v-2"/><path d="M4 14v-2"/><path d="M4 8V6a2 2 0 0 1 2-2h2"/><path d="M8 10h6"/><path d="M8 14h8"/><path d="M8 18h5"/>),
flag_left: %(<path d="M17 22V2L7 7l10 5"/>),
ticket: %(<path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/>),
key_square: %(<path d="M12.4 2.7a2.5 2.5 0 0 1 3.4 0l5.5 5.5a2.5 2.5 0 0 1 0 3.4l-3.7 3.7a2.5 2.5 0 0 1-3.4 0L8.7 9.8a2.5 2.5 0 0 1 0-3.4z"/><path d="m14 7 3 3"/><path d="m9.4 10.6-6.814 6.814A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814"/>),
# Pagination # Pagination
chevron_left: %(<path d="m15 18-6-6 6-6"/>), chevron_left: %(<path d="m15 18-6-6 6-6"/>),
@ -28,7 +39,9 @@ module IconHelper
ellipsis: %(<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>), ellipsis: %(<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>),
# Posts # Posts
search: %(<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>),
fullscreen: %(<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><rect width="10" height="8" x="7" y="8" rx="1"/>), fullscreen: %(<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><rect width="10" height="8" x="7" y="8" rx="1"/>),
anchor: %(<path d="M12 22V8"/><path d="M5 12H2a10 10 0 0 0 20 0h-3"/><circle cx="12" cy="5" r="3"/>),
}.freeze }.freeze
def svg_icon(name, *args) def svg_icon(name, *args)

View File

@ -1,6 +1,29 @@
# frozen_string_literal: true # frozen_string_literal: true
module PaginationHelper module PaginationHelper
def approximate_count(records)
return "" if records.pagination_mode != :numbered
if records.total_pages > records.max_numbered_pages
pages = records.max_numbered_pages
schar = "over "
count = pages * records.records_per_page
title = "Over #{count} results found.\nActual result count may be much larger."
else
pages = records.total_pages
schar = "~"
count = pages * records.records_per_page
title = "Approximately #{count} results found.\nActual result count may differ."
end
tag.span(class: "approximate-count", title: title, data: { count: count, pages: pages, per: records.max_numbered_pages }) do
concat schar
concat number_to_human(count, precision: 2, format: "%n%u", units: { thousand: "k" })
concat " "
concat "result".pluralize(count)
end
end
def sequential_paginator(records) def sequential_paginator(records)
tag.nav(class: "pagination sequential", aria: { label: "Pagination" }) do tag.nav(class: "pagination sequential", aria: { label: "Pagination" }) do
return "" if records.try(:none?) return "" if records.try(:none?)
@ -64,12 +87,12 @@ module PaginationHelper
html = "".html_safe html = "".html_safe
if disabled if disabled
html << tag.span(class: "next", id: "paginator-next", data: { shortcut: "a left" }) do html << tag.span(class: "next", id: "paginator-next", data: { shortcut: "d right" }) do
concat tag.span("Next") concat tag.span("Next")
concat svg_icon(:chevron_right) concat svg_icon(:chevron_right)
end end
else else
html << link_to(link, class: "next", id: "paginator-prev", rel: "next", data: { shortcut: "a left" }) do html << link_to(link, class: "next", id: "paginator-next", rel: "next", data: { shortcut: "d right" }) do
concat tag.span("Next") concat tag.span("Next")
concat svg_icon(:chevron_right) concat svg_icon(:chevron_right)
end end

View File

@ -11,4 +11,47 @@ module UsersHelper
domain = email.split("@").last domain = email.split("@").last
link_to "»", users_path(search: { email_matches: "*@#{domain}" }) link_to "»", users_path(search: { email_matches: "*@#{domain}" })
end end
def simple_avatar(user, **options)
return "" if user.nil?
post_id = user.avatar_id
deferred_post_ids.add(post_id) if post_id
klass = options.delete(:class)
named = options.delete(:named)
tag.a href: user_path(user), class: "simple-avatar placeholder #{klass}", data: { id: post_id, name: user.name } do
tag.span(class: "avatar-button") do
concat tag.span(user.pretty_name, class: "avatar-name") if named
concat tag.span(class: "avatar-image", data: { name: user.name[0].capitalize })
end
end
end
def profile_avatar(user, **options)
return if user.nil?
post_id = user.avatar_id
deferred_post_ids.add(post_id) if post_id
klass = options.delete(:class)
render "/application/profile_avatar", user: user, post_id: post_id, klass: klass
end
def user_level_badge(user)
return if user.nil?
tag.span(class: "level-badge level-#{user.level_string.downcase}") do
user.level_string.upcase
end
end
def user_feedback_badge(user)
return if user.nil?
feedbacks = user.feedback_pieces
deleted = CurrentUser.user.is_staff? ? feedbacks[:deleted] : 0
active = feedbacks[:positive] + feedbacks[:neutral] + feedbacks[:negative]
render "/application/feedback_badge", user: user, positive: feedbacks[:positive], neutral: feedbacks[:neutral], negative: feedbacks[:negative], deleted: deleted, active: active
end
end end

View File

@ -67,6 +67,13 @@ PostSearch.initialize_controls = function () {
$("body").attr("data-st-fullscreen", fullscreen); $("body").attr("data-st-fullscreen", fullscreen);
LStorage.Posts.Fullscreen = fullscreen; LStorage.Posts.Fullscreen = fullscreen;
}); });
let stickySearch = LStorage.Posts.StickySearch;
$("#search-sticky").on("click", () => {
stickySearch = !stickySearch;
$("body").attr("data-st-ssearch", stickySearch);
LStorage.Posts.StickySearch = stickySearch;
});
}; };
$(() => { $(() => {

View File

@ -0,0 +1,170 @@
class Tabs {
constructor ($element) {
this.$menu = $element;
const id = this.$menu.attr("id");
const pagesWrap = $(`tabs-content[for="${id}"]`);
if (!pagesWrap) {
console.error("E6.Tabs", "No content");
return;
}
// Create page and search indices
this.index = new TabIndex(pagesWrap, this.$menu);
// Bootstrap search
const input = this.$menu.find("input[name='search']").on("input", (event, crabs) => {
const val = (input.val() + "").trim();
this.index.find(val);
// Set the query param
if (crabs) return;
const url = new URL(window.location);
if (!val) url.searchParams.delete("find");
else url.searchParams.set("find", input.val() + "");
url.searchParams.delete("tab");
window.history.pushState({}, "", url);
});
// Bootstrap tab buttons
const firstButtonName = this.$menu.find("button").first().attr("name");
this.$menu.on("click", "button", (event, crabs) => {
const button = $(event.currentTarget);
const name = button.attr("name");
if (!name) return;
// Toggle the page
this.index.openPage(name);
this.$menu.find("button.active").removeClass("active");
button.addClass("active");
input.val("");
// Set the query param
if (crabs) return;
const url = new URL(window.location);
if (name == firstButtonName) url.searchParams.delete("tab");
else url.searchParams.set("tab", name);
url.searchParams.delete("find");
window.history.pushState({}, "", url);
});
// Attempt to restore previous state
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.get("tab")) {
// No error handling: if the button does not exist nothing loads
this.$menu.find(`button[name="${queryParams.get("tab")}"]`).trigger("click", [ true ]);
input.val("");
} else if (queryParams.get("find")) {
input.val(queryParams.get("find"), [ true ]);
input.trigger("input");
} else {
// Just open the first tab
this.$menu.find("button").first().click();
input.val("");
}
}
}
class TabIndex {
constructor (wrapper, $menu) {
this.$menu = $menu;
this.pages = {};
this.search = {};
this._allEntries = wrapper.children("tab-entry");
for (const one of this._allEntries) {
const $one = $(one);
const tab = $one.attr("tab");
if (tab) {
if (!this.pages[tab]) this.pages[tab] = [];
this.pages[tab].push($one);
}
// TODO Not a great way of doing this.
// Entries with the same search string will get overwriten.
const search = $one.attr("search");
if (search) this.search[search] = $one;
}
this.groups = {};
this._allGroups = wrapper.children("tab-group");
for (const one of this._allGroups) {
const $one = $(one);
const name = $one.attr("name");
if (!name) return;
this.groups[name] = $one;
}
}
/**
* Show all entries on a specific tab
* @param {string} name Tab name
*/
openPage (name) {
if (!name || !this.pages[name]) {
console.error("E6.Tabs", name, "does not exist");
return;
}
// Activate tab entries
const groups = new Set();
this._allEntries.removeClass("active");
for (const entry of this.pages[name]) {
entry.addClass("active");
if (entry.attr("group"))
groups.add(entry.attr("group"));
}
// Activate group headers
this._allGroups.removeClass("active");
for (const group of groups)
this.groups[group].addClass("active");
}
/**
* Find settings inputs based on keywords
* @param {string} query Search query
*/
find (query) {
this._allEntries.removeClass("active");
this._allGroups.removeClass("active");
// Restore the previous session
if (query.length == 0) {
this.$menu.find("button").first().trigger("click", [ false ]);
return;
}
const terms = query.split(" ");
const groups = new Set();
for (const [tags, $element] of Object.entries(this.search)) {
for (const term of terms) {
if (!tags.includes(term)) continue;
$element.addClass("active");
if ($element.attr("group"))
groups.add($element.attr("group"));
}
}
// Activate group headers
this._allGroups.removeClass("active");
for (const group of groups)
this.groups[group].addClass("active");
}
}
$(() => {
for (const one of $("tabs-menu"))
new Tabs($(one));
});

View File

@ -39,8 +39,8 @@ Takedown.add_posts_by_tags_preview = function (id) {
$("#takedown-add-posts-tags-warning").html(preview_text).show(); $("#takedown-add-posts-tags-warning").html(preview_text).show();
$("#takedown-add-posts-tags").prop("disabled", true); $("#takedown-add-posts-tags").prop("disabled", true);
$("#takedown-add-posts-tags-preview").hide(); $("#takedown-add-posts-tags-preview").hide();
$("#takedown-add-posts-tags-confirm").show(); $("#takedown-add-posts-tags-confirm").css("display", "inline-block");
$("#takedown-add-posts-tags-cancel").show(); $("#takedown-add-posts-tags-cancel").css("display", "inline-block");
}).fail(function (data) { }).fail(function (data) {
Utility.error(data.responseText); Utility.error(data.responseText);
}); });

View File

@ -3,17 +3,23 @@ import LStorage from "./utility/storage";
const Theme = {}; const Theme = {};
Theme.Values = ["Main", "Extra", "StickyHeader", "ForumNotif", "Palette", "Navbar", "Gestures"]; Theme.Values = {
"Theme": ["Main", "Extra", "Palette", "Font", "StickyHeader", "Navbar", "Gestures", "ForumNotif"],
"Posts": ["WikiExcerpt", "StickySearch"],
};
for (const one of Theme.Values) { for (const [label, settings] of Object.entries(Theme.Values)) {
Object.defineProperty(Theme, one, { for (const one of settings) {
get () { return LStorage.Theme[one]; }, Object.defineProperty(Theme, one, {
set (value) { get () { return LStorage.Theme[one]; },
// No value checking, we die like men set (value) {
LStorage.Theme[one] = value; // This has the unintended side effect of setting
$("body").attr("data-th-" + one.toLowerCase(), value); // attribute values that don't exist on the body.
}, LStorage[label][one] = value;
}); $("body").attr("data-th-" + one.toLowerCase(), value);
},
});
}
} }
Theme.initialize_selector = function () { Theme.initialize_selector = function () {
@ -24,13 +30,15 @@ Theme.initialize_selector = function () {
return false; return false;
} }
for (const one of Theme.Values) { for (const [label, settings] of Object.entries(Theme.Values)) {
$("#theme_" + one.toLowerCase()) for (const one of settings)
.val(LStorage.Theme[one] + "") $(`#${label}_${one}`)
.on("change", (event) => { .val(LStorage[label][one] + "")
const data = event.target.value; .on("change", (event) => {
Theme[one] = data; const data = event.target.value;
}); console.log("change", one, data);
Theme[one] = data;
});
} }
}; };
@ -42,12 +50,6 @@ Theme.initialize_buttons = function () {
LStorage.Site.Mascot = 0; LStorage.Site.Mascot = 0;
$("#mascot-value").text(LStorage.Site.Mascot); $("#mascot-value").text(LStorage.Site.Mascot);
}); });
$("#wiki-excerpt-value").text(LStorage.Posts.WikiExcerpt);
$("#wiki-excerpt-reset").on("click", () => {
LStorage.Posts.WikiExcerpt = 1;
$("#wiki-excerpt-value").text(LStorage.Posts.WikiExcerpt);
});
}; };
$(() => { $(() => {

View File

@ -9,8 +9,9 @@ Thumbnails.initialize = function () {
const replacedPosts = []; const replacedPosts = [];
// Avatar special case // Avatar special case
for (const post of $(".simple-avatar")) { for (const post of $(".simple-avatar.placeholder, .profile-avatar.placeholder")) {
const $post = $(post); const $post = $(post);
$post.removeClass("placeholder");
const postID = $post.data("id"); const postID = $post.data("id");
if (!postID) continue; if (!postID) continue;
@ -20,7 +21,11 @@ Thumbnails.initialize = function () {
$("<img>") $("<img>")
.attr("src", postData["preview_url"]) .attr("src", postData["preview_url"])
.appendTo($post.find("span.simple-avatar-image")); .appendTo($post.find("span.avatar-image"));
if ($post.hasClass("profile-avatar"))
$post.attr("href", "/posts/" + postID);
continue; continue;
} }

View File

@ -0,0 +1,25 @@
import LStorage from "./utility/storage";
const Users = {};
Users.init_section = function ($wrapper) {
const $header = $wrapper.find(".profile-section-header").first();
const $body = $(".profile-section-body").first();
const name = $wrapper.attr("name");
if (!name || !$header.length || !$body.length) return;
let state = LStorage.Users[name];
if (state) $wrapper.removeClass("hidden");
$header.on("click", () => {
$wrapper.toggleClass("hidden", state);
state = !state;
LStorage.Users[name] = state;
});
};
$(() => {
for (const one of $((".profile-section")))
Users.init_section($((one)));
});

View File

@ -57,6 +57,9 @@ LStorage.Theme = {
/** @returns {string} Colorblind-friendly palette (default / deut / trit) */ /** @returns {string} Colorblind-friendly palette (default / deut / trit) */
Palette: ["theme-palette", "default"], Palette: ["theme-palette", "default"],
/** @returns {string} Font family (verdana / leto / lexend / dyslexic ) */
Font: ["theme-font", "Verdana"],
/** @returns {string} Position of the navbar on the post page (top / bottom / both / none) */ /** @returns {string} Position of the navbar on the post page (top / bottom / both / none) */
Navbar: ["theme-nav", "top"], Navbar: ["theme-nav", "top"],
@ -91,6 +94,9 @@ LStorage.Posts = {
/** @returns {boolean} True if the search should be displayed in fullscreen */ /** @returns {boolean} True if the search should be displayed in fullscreen */
Fullscreen: ["e6.posts.fusk", false], Fullscreen: ["e6.posts.fusk", false],
/** @returns {boolean} True if the search should be displayed in fullscreen */
StickySearch: ["e6.posts.ssearch", false],
}; };
StorageUtils.bootstrapMany(LStorage.Posts); StorageUtils.bootstrapMany(LStorage.Posts);
@ -184,6 +190,17 @@ LStorage.Blacklist = {
StorageUtils.bootstrapSome(LStorage.Blacklist, ["Collapsed"]); StorageUtils.bootstrapSome(LStorage.Blacklist, ["Collapsed"]);
// Users page config
LStorage.Users = {
/** @returns {boolean} True to show staff stats, false to hide them */
StaffStats: ["e6.users.staffstats", false],
/** @returns {boolean} True to show user stats, false to hide them */
StaffNotes: ["e6.users.staffnotes", false],
};
StorageUtils.bootstrapMany(LStorage.Users);
/** /**
* Patches the add, delete, and clear methods for the filter cache set. * Patches the add, delete, and clear methods for the filter cache set.
* Otherwise, modifying the set with these methods would not update the local storage * Otherwise, modifying the set with these methods would not update the local storage

View File

@ -8,6 +8,7 @@
@import "base/base"; @import "base/base";
@import "base/links"; @import "base/links";
@import "base/fontawesome"; @import "base/fontawesome";
@import "base/fonts";
@import "common/standard_variables"; @import "common/standard_variables";
@import "common/standard_elements"; @import "common/standard_elements";
@ -42,6 +43,10 @@
@import "common/user_styles.scss"; @import "common/user_styles.scss";
@import "common/voting.scss"; @import "common/voting.scss";
@import "views/application/application";
@import "views/posts/posts";
@import "views/users/users";
@import "specific/admin_dashboards.scss"; @import "specific/admin_dashboards.scss";
@import "specific/api_keys.scss"; @import "specific/api_keys.scss";
@import "specific/artists.scss"; @import "specific/artists.scss";
@ -68,8 +73,6 @@
@import "specific/popular.scss"; @import "specific/popular.scss";
@import "specific/post_delete.scss"; @import "specific/post_delete.scss";
@import "specific/post_flags.scss"; @import "specific/post_flags.scss";
@import "specific/post_index.scss";
@import "specific/post_mode_menu.scss";
@import "specific/posts.scss"; @import "specific/posts.scss";
@import "specific/post_replacements.scss"; @import "specific/post_replacements.scss";
@import "specific/post_versions.scss"; @import "specific/post_versions.scss";

View File

@ -0,0 +1,161 @@
// Picker
body {
&[data-th-font="Lato"] { font-family: "Lato", $base_font_family; }
&[data-th-font="Lexend"] { font-family: "Lexend", $base_font_family; }
&[data-th-font="Monospace"] { font-family: monospace, monospace; }
&[data-th-font="OpenDyslexic"] { font-family: "OpenDyslexic", $base_font_family; }
&[data-th-font="OpenSans"] { font-family: "OpenSans", $base_font_family; }
&[data-th-font="ComicSans"] { font-family: "Comic Sans MS", "Comic Sans", cursive; }
}
/** Font Definitions **/
// Lato
@font-face {
font-family: "Lato";
src: url("/public/fonts/Lato/Lato-Regular.woff2") format("woff2"),
url("/public/fonts/Lato/Lato-Regular.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Lato";
src: url("/public/fonts/Lato/Lato-Bold.woff2") format("woff2"),
url("/public/fonts/Lato/Lato-Bold.woff") format("woff");
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Lato";
src: url("/public/fonts/Lato/Lato-Italic.woff2") format("woff2"),
url("/public/fonts/Lato/Lato-Italic.woff") format("woff");
font-weight: normal;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Lato";
src: url("/public/fonts/Lato/Lato-BoldItalic.woff2") format("woff2"),
url("/public/fonts/Lato/Lato-BoldItalic.woff") format("woff");
font-weight: bold;
font-style: italic;
font-display: swap;
}
// Lexend
@font-face {
font-family: "Lexend";
src: url("/public/fonts/Lexend/Lexend-Regular.woff2") format("woff2"),
url("/public/fonts/Lexend/Lexend-Regular.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Lexend";
src: url("/public/fonts/Lexend/Lexend-Bold.woff2") format("woff2"),
url("/public/fonts/Lexend/Lexend-Bold.woff") format("woff");
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Lexend";
src: url("/public/fonts/Lexend/Lexend-Italic.woff2") format("woff2"),
url("/public/fonts/Lexend/Lexend-Italic.woff") format("woff");
font-weight: normal;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Lexend";
src: url("/public/fonts/Lexend/Lexend-BoldItalic.woff2") format("woff2"),
url("/public/fonts/Lexend/Lexend-BoldItalic.woff") format("woff");
font-weight: bold;
font-style: italic;
font-display: swap;
}
// OpenDyslexic
@font-face {
font-family: "OpenDyslexic";
src: url("/public/fonts/OpenDyslexic/OpenDyslexic-Regular.woff2") format("woff2"),
url("/public/fonts/OpenDyslexic/OpenDyslexic-Regular.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "OpenDyslexic";
src: url("/public/fonts/OpenDyslexic/OpenDyslexic-Bold.woff2") format("woff2"),
url("/public/fonts/OpenDyslexic/OpenDyslexic-Bold.woff") format("woff");
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "OpenDyslexic";
src: url("/public/fonts/OpenDyslexic/OpenDyslexic-Italic.woff2") format("woff2"),
url("/public/fonts/OpenDyslexic/OpenDyslexic-Italic.woff") format("woff");
font-weight: normal;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "OpenDyslexic";
src: url("/public/fonts/OpenDyslexic/OpenDyslexic-BoldItalic.woff2") format("woff2"),
url("/public/fonts/OpenDyslexic/OpenDyslexic-BoldItalic.woff") format("woff");
font-weight: bold;
font-style: italic;
font-display: swap;
}
// OpenSans
@font-face {
font-family: "OpenSans";
src: url("/public/fonts/OpenSans/OpenSans-Regular.woff2") format("woff2"),
url("/public/fonts/OpenSans/OpenSans-Regular.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "OpenSans";
src: url("/public/fonts/OpenSans/OpenSans-Bold.woff2") format("woff2"),
url("/public/fonts/OpenSans/OpenSans-Bold.woff") format("woff");
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "OpenSans";
src: url("/public/fonts/OpenSans/OpenSans-Italic.woff2") format("woff2"),
url("/public/fonts/OpenSans/OpenSans-Italic.woff") format("woff");
font-weight: normal;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "OpenSans";
src: url("/public/fonts/OpenSans/OpenSans-BoldItalic.woff2") format("woff2"),
url("/public/fonts/OpenSans/OpenSans-BoldItalic.woff") format("woff");
font-weight: bold;
font-style: italic;
font-display: swap;
}

View File

@ -19,3 +19,15 @@
@function themed($key) { @function themed($key) {
@return var(--#{$key}); @return var(--#{$key});
} }
@mixin with-theme($key, $value) {
body[data-th-#{$key}="#{$value}"] {
@content;
}
}
@mixin with-setting($key, $value) {
body[data-st-#{$key}="#{$value}"] {
@content;
}
}

View File

@ -20,7 +20,7 @@ $dtext_h3_size: 1.6em;
$dtext_h4_size: 1.4em; $dtext_h4_size: 1.4em;
$dtext_h5_size: 1.2em; $dtext_h5_size: 1.2em;
$dtext_h6_size: 1em; $dtext_h6_size: 1em;
$base_font_family: Verdana, sans-serif; $base_font_family: Verdana, Geneva, sans-serif;
$box-shadow-size: 2px 2px 5px; $box-shadow-size: 2px 2px 5px;

View File

@ -29,7 +29,7 @@ footer.footer-wrapper {
margin-right: -3.25rem; margin-right: -3.25rem;
background: themed("color-background") themed("image-background"); background: themed("color-background") themed("image-background");
border-radius: 50%; border-radius: 25%;
padding: 0.5rem; padding: 0.5rem;
} }
} }

View File

@ -21,11 +21,12 @@
line-height: st-value(100); line-height: st-value(100);
padding: st-value(100) / 2; padding: st-value(100) / 2;
height: st-value(100) * 2; height: st-value(100) * 2;
box-sizing: border-box;
// TODO What if button is on a light background // TODO What if button is on a light background
background: $button-background; background: $button-background;
color: $button-text-color; color: $button-text-color;
&:hover { background: $button-background-hover; } &:hover, &:active { background: $button-background-hover; }
& > svg { & > svg {
// Icon should be slightly larger than text, // Icon should be slightly larger than text,
@ -64,3 +65,57 @@
padding: (st-value(100) / 2) 0; padding: (st-value(100) / 2) 0;
} }
} }
// Colored buttons
.st-button.danger {
background: palette("background-red");
&:hover, &:active {
background: palette("background-red-d5")
}
}
// Toggle switch
label.st-toggle {
display: flex;
width: 2.5rem;
height: 0.75rem;
box-sizing: border-box;
position: relative;
background-color: themed("color-foreground");
// transition: background-color 200ms;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0;
box-shadow: inset 0 0 0.25rem #00000060;
&::after {
content: "";
width: 1rem;
height: 1rem;
position: absolute;
top: -0.125rem;
left: 0rem;
background: themed("color-link");
border-radius: 0.25rem;
transition: left 100ms, background-color 200ms;
}
}
input[type="checkbox"][disabled].st-toggle + label.st-toggle {
background: palette("background-grey");
cursor: not-allowed;
}
input[type="checkbox"].st-toggle { display: none; }
input[type="checkbox"].st-toggle:checked + label.st-toggle {
// background-color: palette("background-green-d5");
&::after {
content: "";
left: 1.5rem;
background: themed("color-link-active");
}
}

View File

@ -17,5 +17,5 @@ $st-values: (
@function padding($value) { @return st-value(($value)); } @function padding($value) { @return st-value(($value)); }
@mixin st-padding($value) { padding: padding(($value)); } @mixin st-padding($value) { padding: padding(($value)); }
@function radius($value) { @return st-value($value); } @function radius($value: 025) { @return st-value($value); }
@mixin st-radius($value) { border-radius: radius($value); } @mixin st-radius($value: 025) { border-radius: radius($value); }

View File

@ -5,7 +5,7 @@ nav.navigation {
grid-template-rows: min-content min-content min-content min-content auto; grid-template-rows: min-content min-content min-content min-content auto;
width: 100%; // otherwise narrow when fixed width: 100%; // otherwise narrow when fixed
z-index: 20; // otherwise post labels layered above z-index: 200; // above post labels and notes
position: relative; position: relative;
@ -76,11 +76,11 @@ nav.navigation {
} }
a.simple-avatar { a.simple-avatar {
.simple-avatar-button { .avatar-button {
padding: 0; padding: 0;
gap: 0; gap: 0;
.simple-avatar-name { .avatar-name {
padding: 0.5rem; padding: 0.5rem;
@include window-smaller-than(32rem) { @include window-smaller-than(32rem) {
@ -88,7 +88,7 @@ nav.navigation {
} }
} }
.simple-avatar-image { .avatar-image {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -119,7 +119,7 @@ nav.navigation {
} }
@include window-smaller-than(32rem) { @include window-smaller-than(32rem) {
&.sign-in .simple-avatar-image { &.sign-in .avatar-image {
background: themed("color-foreground"); background: themed("color-foreground");
} }
} }
@ -187,6 +187,8 @@ nav.navigation {
padding: 1rem 0.5rem; padding: 1rem 0.5rem;
} }
li.current a { background-color: themed("color-foreground"); } li.current a { background-color: themed("color-foreground"); }
li.nav-hidden { display: none; }
} }
.nav-secondary { .nav-secondary {
@ -493,18 +495,18 @@ nav.navigation, html.nav-toggled nav.navigation {
padding: 0; padding: 0;
height: 100%; height: 100%;
.simple-avatar-button { .avatar-button {
background: none; background: none;
color: inherit; color: inherit;
align-items: start; align-items: start;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 0.875rem; line-height: 0.875rem;
.simple-avatar-name { .avatar-name {
padding: 0 0.5rem; padding: 0 0.5rem;
} }
.simple-avatar-image { .avatar-image {
height: 3rem; height: 3rem;
width: 3rem; width: 3rem;
background: themed("color-foreground"); background: themed("color-foreground");
@ -526,9 +528,9 @@ nav.navigation, html.nav-toggled nav.navigation {
} }
// Stage 2: account label // Stage 2: account label
.collapse-2 .simple-avatar-name { display: none; } .collapse-2 .avatar-name { display: none; }
@include window-larger-than(65rem) { @include window-larger-than(65rem) {
.collapse-2 .simple-avatar-name { display: unset; } .collapse-2 .avatar-name { display: unset; }
} }
} }
} }
@ -562,10 +564,10 @@ body.c-static.a-home {
position: static; position: static;
height: unset; height: unset;
padding: 0; padding: 0;
.simple-avatar-button { .avatar-button {
height: unset; height: unset;
align-items: center; align-items: center;
.simple-avatar-image { display: none; } .avatar-image { display: none; }
} }
} }
} }

View File

@ -16,7 +16,7 @@ div#notice {
top: 1rem; top: 1rem;
left: 25%; left: 25%;
width: 50%; width: 50%;
z-index: 100; z-index: 500;
color: themed("color-text"); color: themed("color-text");
background-color: themed("color-success"); background-color: themed("color-success");

View File

@ -102,7 +102,7 @@ nav.pagination {
// Sequential paginator // Sequential paginator
.paginator.sequential { nav.pagination.sequential {
gap: 5rem; gap: 5rem;
a span { display: block; } a span { display: block; }
} }

View File

@ -13,7 +13,7 @@
textarea { textarea {
// Override default texarea styles // Override default texarea styles
font-family: Verdana, sans-serif; font-family: $base_font_family;
font-size: 1rem; font-size: 1rem;
line-height: 1rem; line-height: 1rem;
padding: 0.5rem 0 0.5rem 0.5rem; padding: 0.5rem 0 0.5rem 0.5rem;
@ -30,7 +30,7 @@
// Disable manual resizing // Disable manual resizing
resize: none; resize: none;
border-radius: 3px 0 0 3px; border-radius: 0.25rem 0 0 0.25rem;
box-sizing: border-box; box-sizing: border-box;
flex: 1; flex: 1;
@ -43,19 +43,27 @@
font-size: unset; font-size: unset;
max-width: unset; max-width: unset;
font-size: 1rem; display: flex;
line-height: 1rem; align-items: center;
padding: 0.5rem; padding: 0.5rem;
border-radius: 0 3px 3px 0; border-radius: 0 0.25rem 0.25rem 0;
background: white; background: white;
color: black;
span { display: none; } svg {
height: 1.5rem;
width: 1.5rem;
margin: -0.25rem 0 -0.25rem;
}
} }
@include window-larger-than(800px) { @include window-larger-than(800px) {
textarea, button[type="submit"] { textarea { font-size: 0.85rem; }
font-size: 0.85rem; button[type="submit"] svg {
height: 1.25rem;
width: 1.25rem;
margin: -0.125rem 0 -0.125rem;
} }
} }
} }

View File

@ -92,8 +92,6 @@ table.search {
} }
table.aligned-vertical { table.aligned-vertical {
@extend .search;
tr { tr {
height: 1.75em; height: 1.75em;
} }

View File

@ -43,7 +43,7 @@ body.c-static.a-home {
input[type="text"] { input[type="text"] {
flex: 1; flex: 1;
border: 0; border: 0;
border-radius: 3px 0 0 3px; border-radius: 0.25rem 0 0 0.25rem;
padding: 0.5rem; padding: 0.5rem;
font-size: 1rem; font-size: 1rem;
@ -57,9 +57,14 @@ body.c-static.a-home {
button[type="submit"] { button[type="submit"] {
background: white; background: white;
border-radius: 0 3px 3px 0; border-radius: 0 0.25rem 0.25rem 0;
padding: 0 0.5em; padding: 0 0.5rem;
font-size: 1rem;
svg {
height: 1.25rem;
width: 1.25rem;
margin: -0.125rem 0 -0.125rem;
}
} }
} }

View File

@ -1,287 +0,0 @@
body.c-posts.a-index, body.c-favorites.a-index {
#page {
// Override the theme to instead
// project it upon the content area
background: none;
padding: 0;
}
// Exhibit A
// Makes the content area take up the
// full height of the page. Yes, really.
#page, #c-posts, #c-favorites, #a-index {
// I hate both this and myself
display: flex;
flex-flow: column;
flex: 1;
}
}
.post-index {
display: grid;
grid-template-areas:
"search "
"content"
"sidebar";
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr min-content;
flex: 1; // See Exhibit A
// 1. Searchbox
.search {
grid-area: search;
padding: 0.5rem 0.25rem;
background-color: #152f56;
background-color: themed("color-foreground");
box-shadow: inset 0px -0.25rem 0.25rem -0.25rem themed("color-background");
h1 {
font-size: $h3-size;
}
.search-controls {
display: none;
flex-flow: column;
}
}
// 2. Content
.content {
display: flex; // See Exhibit A
flex-flow: column;
grid-area: content;
// Imported from #page
padding: 0.5rem 0.25rem themed("content-padding-bottom");
background-color: #152f56;
background-color: themed("color-foreground");
background-image: themed("image-foreground");
background-position: themed("image-foreground-position");
background-repeat: themed("image-foreground-repeat");
// Quick tag edit
#edit-dialog textarea {
margin-bottom: 0.25rem;
}
// Actual content area:
// posts and pagination
.post-index-gallery {
display: flex;
flex-flow: column;
gap: 1rem;
flex: 1; // See Exhibit A
.posts-container {
flex: 1; // See Exhibit A
grid-auto-rows: min-content;
}
}
}
// 3. Sidebar
.sidebar {
grid-area: sidebar;
display: flex;
flex-flow: column;
gap: 1em;
padding: 0.5rem 0.25rem;
background-color: #152f56;
background-color: themed("color-foreground");
box-shadow: inset 0px 0.25rem 0.25rem -0.25rem themed("color-background");
// Mode selection
#mode-box-mode, #mode-box #set-id {
width: 100%;
// Match the searchbox
padding: 0.5em;
font-family: Verdana, sans-serif;
font-size: 1.05em;
}
}
}
// Desktop
.post-index {
@include window-larger-than(50rem) {
grid-template-areas:
"search content"
"sidebar content";
grid-template-columns: 14rem 1fr;
grid-template-rows: min-content 1fr;
.search {
box-shadow: inset -0.25rem 0px 0.25rem -0.25rem themed("color-background");
margin-top: 0.25rem;
border-top-left-radius: 0.25rem;
padding: 0.5rem;
.search-controls {
display: flex;
margin-top: 0.5rem;
}
}
.sidebar {
box-shadow: inset -0.25rem 0px 0.25rem -0.25rem themed("color-background");
margin-bottom: 0.25rem;
border-bottom-left-radius: 0.25rem;
padding: 0.5rem
}
.content {
border-radius: 0.25rem;
}
}
}
// Fullscreen
body.c-posts.a-index[data-st-fullscreen="true"] {
// Desktop-only, for obvious reasons
@include window-larger-than(50rem) {
.post-index {
grid-template-areas:
"search "
"content";
grid-template-columns: 1fr;
.search {
display: flex;
border-radius: 0.25rem 0.25rem 0 0;
box-shadow: inset 0px -0.25rem 0.25rem -0.25rem themed("color-background");
.post-search {
flex: 1;
}
.search-controls {
display: flex;
justify-content: right;
align-self: end;
margin: 0 0 0 0.5rem;
.st-button.w100 {
width: unset;
span { display: none; }
}
}
}
.sidebar { display: none; }
.content {
border-radius: 0 0 0.25rem 0.25rem;
}
}
}
}
// FEATURES
// Wiki Excerpt
.wiki-excerpt {
display: flex;
flex-flow: column;
position: relative;
background: themed("color-section");
border-radius: 0.25rem;
&.hidden { display: none; }
// header
h3 {
display: flex;
align-items: center;
font-size: 1rem;
padding: 0.5rem;
cursor: pointer;
& > svg {
transition: transform 0.25s;
height: 1.5rem;
width: 1.5rem;
}
.wiki-excerpt-dismiss { margin-left: auto; }
}
// body
.styled-dtext {
background: linear-gradient(to top, themed("color-section"), themed("color-text"));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
min-height: 0rem;
max-height: 0rem;
max-width: 50rem;
overflow: hidden;
padding: 0 0.5rem;
transition: max-height 0.25s;
// Disable links
pointer-events: none;
cursor: unset;
a {
color: unset;
text-decoration: underline;
&::after { content: none; }
}
p:last-child { margin-bottom: 0; }
}
// wiki link
.wiki-excerpt-readmore {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3rem;
max-width: 50rem;
box-sizing: border-box;
// Makes the button appear in the middle of the animation
transition: visibility 0s 0.125s;
visibility: hidden;
span {
padding: 0.5rem 1rem;
background: themed("color-section");
border-radius: 0.25rem;
}
}
&.open{
.wiki-excerpt-toggle svg { transform: rotate(90deg); }
.styled-dtext {
max-height: 10rem;
min-height: 2rem;
}
.wiki-excerpt-readmore { visibility: visible; }
}
&.loading {
h3::after, .styled-dtext { transition: none; }
}
}

View File

@ -15,7 +15,7 @@ div#c-users {
gap: 1em; gap: 1em;
& > div { & > div {
max-width: 1600px; max-width: 100rem;
box-sizing: border-box; box-sizing: border-box;
} }
@ -76,107 +76,7 @@ div#c-users {
// Middle section: uploads and favorites // Middle section: uploads and favorites
.blacklist-ui { padding: 0; } .blacklist-ui { padding: 0; }
.posts-section {
display: flex;
flex-flow: column;
gap: 1em;
.profile-sample {
display: grid;
grid-template: "p-header"
"p-links"
"p-posts";
gap: 0.5em 0;
@include window-larger-than(800px) {
grid-template: "p-header p-links"
"p-posts p-posts";
grid-template-columns: 12em 1fr;
gap: 0 0.5em;
}
}
.profile-sample-header a, .profile-sample-links a {
display: block;
box-sizing: border-box;
align-content: center;
text-align: center;
height: 100%;
padding: 0.5em;
border-radius: 6px;
background-color: themed("color-section");
&:hover {
background-color: themed("color-section-lighten-5");
}
&:focus, &:active {
outline: 0;
color: themed("color-link-active");
}
}
.profile-sample-header {
grid-area: p-header;
display: flex;
a {
font-size: 1.25em;
font-weight: bold;
width: 100%;
}
@include window-larger-than(800px) {
a { border-radius: 6px 6px 0 0; }
}
}
.profile-sample-links {
grid-area: p-links;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
text-align: center;
span {
padding: 0.5em;
color: themed("color-text-muted");
}
.spacer { display: none; }
.profile-comments-link {
grid-row: 1 / 3;
grid-column: 3;
}
@include window-larger-than(800px) {
display: flex;
a { height: min-content; }
.spacer { display: block; }
}
}
.profile-sample-posts {
grid-area: p-posts;
display: flex;
overflow: auto;
justify-content: center;
flex-wrap: wrap;
gap: 1em;
padding: 0.5em;
background: var(--color-section);
border-radius: 6px;
// Desktop
@include window-larger-than(800px) {
flex-wrap: nowrap;
border-top-left-radius: 0;
justify-content: flex-start;
}
}
}
// Bottom section: about me and commission info // Bottom section: about me and commission info
@ -200,29 +100,6 @@ div#c-users {
} }
} }
} }
div#a-edit {
h1 {
margin: 0.5em 0;
}
h2 {
margin: 0.5em 0;
}
div.input {
margin-bottom: 2em;
}
div.input span.hint {
display: block;
max-width: 70%;
}
.active {
color: themed("color-link-active");
}
}
} }
// User signup and login // User signup and login

View File

@ -0,0 +1,3 @@
@import "avatar";
@import "feedback_badge";
@import "level_badge";

View File

@ -0,0 +1,41 @@
.profile-avatar {
.avatar-image {
display: flex;
width: 5rem;
height: 5rem;
position: relative;
background: themed("color-section");
@include st-radius;
// Letter if no avatar image
&::after {
content: attr(data-initial);
display: flex;
justify-content: center;
align-items: center;
font-size: 5rem;
font-weight: bold;
color: themed("color-foreground");
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
// On top of the letter
img {
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
@include st-radius;
}
}
}

View File

@ -0,0 +1,103 @@
$hex-size: 1.25rem;
.user-record {
display: flex;
height: $hex-size * 0.6; // 0.75rem
width: $hex-size; // 1.25rem
margin: ($hex-size / 5) 0; // 0.25rem 0
justify-content: center;
align-items: center;
font-size: ($hex-size / 5 * 4) * 0.9;
svg { width: ($hex-size / 5 * 4) * 0.9; }
background: palette("plain-black");
color: white;
// Place corners under the text
position: relative;
z-index: 1;
&::before, &::after {
content: "";
position: absolute;
right: 0;
left: 0;
border-left: ($hex-size / 2) solid transparent;
border-right: ($hex-size / 2) solid transparent;
z-index: -1;
}
&::before {
top: -($hex-size / 5);
border-bottom: ($hex-size / 5) solid palette("plain-black");
}
&::after {
bottom: -($hex-size / 5);
border-top: ($hex-size / 5) solid palette("plain-black");
}
// Variations
&.deleted {
background: palette("background-yellow");
&::before { border-bottom-color: palette("background-yellow"); }
&::after { border-top-color: palette("background-yellow"); }
}
&.negative {
background: palette("background-red");
&::before { border-bottom-color: palette("background-red"); }
&::after { border-top-color: palette("background-red"); }
}
&.neutral {
background: palette("background-grey");
&::before { border-bottom-color: palette("background-grey"); }
&::after { border-top-color: palette("background-grey"); }
}
&.positive {
background: palette("background-green");
&::before { border-bottom-color: palette("background-green"); }
&::after { border-top-color: palette("background-green"); }
}
}
.user-records-list {
display: flex;
flex-flow: row;
gap: 0.25rem;
&:hover .user-record {
&.deleted {
background: palette("background-yellow-d5");
&::before { border-bottom-color: palette("background-yellow-d5"); }
&::after { border-top-color: palette("background-yellow-d5"); }
}
&.negative {
background: palette("background-red-d5");
&::before { border-bottom-color: palette("background-red-d5"); }
&::after { border-top-color: palette("background-red-d5"); }
}
&.neutral {
background: palette("background-grey-d5");
&::before { border-bottom-color: palette("background-grey-d5"); }
&::after { border-top-color: palette("background-grey-d5"); }
}
&.positive {
background: palette("background-green-d5");
&::before { border-bottom-color: palette("background-green-d5"); }
&::after { border-top-color: palette("background-green-d5"); }
}
}
}

View File

@ -0,0 +1,40 @@
$user-levels: (
"unactivated"
"blocked"
"member"
"privileged"
"former-staff"
"janitor"
"moderator"
"admin"
);
$user-level-text: (
"moderator": #ffffff,
"janitor": #ffffff,
);
.level-badge {
font-weight: bold;
font-size: 0.65rem;
color: black;
background: themed("color-user-member");
padding: 0.1rem 0.25rem;
@include st-radius;
@each $level in $user-levels {
&.level-#{$level} {
background: themed("color-user-" + $level);
}
}
@each $level, $color in $user-level-text {
&.level-#{$level} {
color: $color;
}
}
&.level-unactivated {
background: themed("palette-background-red");
color: #ffffff;
}
}

View File

@ -0,0 +1,19 @@
// Main themes
@import "index/index";
@include window-larger-than(50rem) {
@import "index/index.desktop";
}
// Features
@import "index/partials/mode_menu";
@import "index/partials/wiki_excerpt";
@import "index/partials/stats";
@include window-larger-than(50rem) {
@include with-setting("fullscreen", "true") {
@import "index/partials/fullscreen";
}
@include with-setting("ssearch", "true") {
@import "index/partials/sticky";
}
}

View File

@ -0,0 +1,57 @@
// Desktop-only
// Rollout at 50rem
.post-index {
grid-template-areas:
"search content"
"sidebar content";
grid-template-columns: 15rem 1fr;
grid-template-rows: min-content 1fr;
.search {
box-shadow: inset -0.25rem 0px 0.25rem -0.25rem themed("color-background");
padding: 0.5rem 0.75rem 0.5rem 0.5rem;
// Align the controls properly
position: relative;
.search-controls {
display: flex;
flex-flow: row;
justify-content: right;
position: absolute;
bottom: -1.633rem;
right: 0.5rem;
padding: 0.25rem;
gap: 0.5rem;
background: themed("color-foreground");
border-radius: 0 0 0.25rem 0.25rem;
button {
height: 1.5rem;
padding: 0;
svg {
height: 1.25rem;
width: 1.25rem;
padding: 0.25rem;
margin: -0.125rem 0;
}
}
}
}
.sidebar {
box-shadow: inset -0.25rem 0px 0.25rem -0.25rem themed("color-background");
padding: 0.5rem 0.75rem 0.5rem 0.5rem;
font-size: 100%;
}
.content {
border-radius: 0 0.25rem 0.25rem 0;
padding: 0.5rem 0.75rem themed("content-padding-bottom");
}
}

View File

@ -0,0 +1,108 @@
body.c-posts.a-index, body.c-favorites.a-index {
#page {
// Override the theme to instead
// project it upon the content area
background: var(--color-foreground);
padding: 0;
}
// Exhibit A
// Makes the content area take up the
// full height of the page. Yes, really.
#page, #c-posts, #c-favorites, #a-index {
// I hate both this and myself
display: flex;
flex-flow: column;
flex: 1;
}
}
// Post gallery
.post-index {
display: grid;
grid-template-areas:
"search "
"content"
"sidebar";
grid-template-columns: 1fr;
grid-template-rows: min-content 1fr min-content;
flex: 1; // See Exhibit A
// 1. Searchbox
.search {
grid-area: search;
padding: 0.5rem 0.25rem;
box-shadow: inset 0px -0.25rem 0.25rem -0.25rem themed("color-background");
h1 {
font-size: $h3-size;
}
.search-controls { display: none; }
}
// 2. Content
.content {
display: flex; // See Exhibit A
flex-flow: column;
grid-area: content;
// Imported from #page
padding: 0.5rem 0.25rem themed("content-padding-bottom");
background-color: #152f56;
background-color: themed("color-foreground");
background-image: themed("image-foreground");
background-position: themed("image-foreground-position");
background-repeat: themed("image-foreground-repeat");
// Quick tag edit
#edit-dialog textarea {
margin-bottom: 0.25rem;
}
// Actual content area:
// posts and pagination
.post-index-gallery {
display: flex;
flex-flow: column;
gap: 1rem;
flex: 1; // See Exhibit A
.posts-container {
flex: 1; // See Exhibit A
grid-auto-rows: min-content;
}
}
}
// 3. Sidebar
.sidebar {
grid-area: sidebar;
display: flex;
flex-flow: column;
gap: 1em;
padding: 0.5rem 0.25rem;
box-shadow: inset 0px 0.25rem 0.25rem -0.25rem themed("color-background");
// By popular demand
font-size: 150%;
// Mode selection
#mode-box-mode, #mode-box #set-id {
width: 100%;
// Match the searchbox
padding: 0.5em;
font-family: $base_font_family;
font-size: 1.05em;
}
}
}

View File

@ -0,0 +1,35 @@
// Fullscreen mode
// Only relevant on desktop
.post-index {
grid-template-areas:
"search "
"content";
grid-template-columns: 1fr;
.search {
display: flex;
border-radius: 0.25rem 0.25rem 0 0;
box-shadow: inset 0px -0.25rem 0.25rem -0.25rem themed("color-background");
margin: 0.25rem 0 0;
padding: 0.5rem;
flex-wrap: wrap;
justify-content: right;
z-index: 11; // above posts and labels
.post-search { width: 100%; }
#search-fullscreen {
background: themed("color-button-active");
color: black;
}
.search-controls { right: 0.25rem; }
}
.sidebar { display: none; }
.content {
border-radius: 0 0 0.25rem 0.25rem;
.posts-index-stats { margin-right: 5rem; }
}
}

View File

@ -32,4 +32,4 @@ $modes: (
input { flex: 1; } input { flex: 1; }
button { padding: 0 0.25em; } button { padding: 0 0.25em; }
} }

View File

@ -0,0 +1,13 @@
.posts-index-stats {
display: flex;
justify-content: end;
margin: -0.5rem 0rem 0.25rem;
font-size: 0.75rem;
line-height: 0.75rem;
color: #fffa;
font-family: monospace;
& > span { cursor: help; }
}

View File

@ -0,0 +1,19 @@
.post-index .search {
position: sticky;
top: 0;
background: themed("color-foreground");
border-radius: 0.25rem;
// on top of thumbnail labels
z-index: 11;
#search-sticky {
background: themed("color-button-active");
color: black;
}
}
&[data-th-sheader="true"] {
.post-index .search { top: 3.75rem; }
}

View File

@ -0,0 +1,101 @@
.wiki-excerpt {
display: flex;
flex-flow: column;
position: relative;
background: themed("color-section");
border-radius: 0.25rem;
&.hidden { display: none; }
// header
h3 {
display: flex;
align-items: center;
font-size: 1rem;
padding: 0.5rem;
cursor: pointer;
& > svg {
transition: transform 0.25s;
height: 1.5rem;
width: 1.5rem;
}
.wiki-excerpt-dismiss { margin-left: auto; }
}
// body
.styled-dtext {
background: linear-gradient(to top, themed("color-section"), themed("color-text"));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
min-height: 0rem;
max-height: 0rem;
max-width: 50rem;
overflow: hidden;
padding: 0 0.5rem;
transition: max-height 0.25s;
// Disable links
pointer-events: none;
cursor: unset;
a {
color: unset;
text-decoration: underline;
&::after { content: none; }
}
// Quote visual bug
blockquote { background: unset; }
// Remove offset caused by paragraphs
p:last-child { margin-bottom: 0; }
}
// wiki link
.wiki-excerpt-readmore {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3rem;
max-width: 50rem;
box-sizing: border-box;
// Makes the button appear in the middle of the animation
transition: visibility 0s 0.125s;
visibility: hidden;
span {
padding: 0.5rem 1rem;
background: themed("color-section");
border-radius: 0.25rem;
}
}
&.open{
.wiki-excerpt-toggle svg { transform: rotate(90deg); }
.styled-dtext {
max-height: 10rem;
min-height: 2rem;
}
.wiki-excerpt-readmore { visibility: visible; }
}
&.loading {
h3::after, .styled-dtext { transition: none; }
}
}

View File

@ -0,0 +1,2 @@
@import "show/show";
@import "edit/edit";

View File

@ -0,0 +1,200 @@
body.c-users.a-edit {
form.simple_form {
background: unset;
padding: unset;
margin: unset;
input[type="text"], input[type="number"], textarea, select {
width: 100%;
max-width: unset;
box-sizing: border-box;
border-radius: 0.25rem;
font-size: 1rem;
padding: 0.25rem;
}
}
#settings-account-buttons tab-body {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
.st-button {
white-space: nowrap;
justify-content: center;
}
}
tab-submit {
input[type="submit"] {
background: themed("color-tag-artist");
font-size: 1.25rem;
padding: 0.25rem 1rem;
margin: 0.5rem;
border-radius: 0.25rem;
&:hover, &:active { background: themed("color-tag-artist-alt"); }
}
}
}
// Tabs section
tabs-menu {
margin-top: 0.5rem;
display: flex;
width: 100%;
max-width: 50rem;
flex-wrap: wrap;
// TODO Smooth drag when overflowing
overflow: auto;
& > button {
background: unset;
color: themed("color-link");
padding: 0.5rem 1rem;
border-bottom: 2px solid themed("color-section");
font-size: 1rem;
line-height: 1rem;
&.active {
background: themed("color-section");
border-radius: 0.25rem 0.25rem 0 0;
&:first-child {
border-bottom-left-radius: 0.25rem;
}
}
}
span.spacer {
flex: 1;
border-bottom: 2px solid themed("color-section");
}
input[name="search"] {
border-radius: 0 0 0.25rem 0.25rem;
font-size: 1rem;
line-height: 1rem;
padding: 0.25rem;
width: 100%;
box-sizing: border-box;
}
}
// Tabs structure
tabs-content {
display: flex;
flex-flow: column;
max-width: 50rem;
tab-group {
display: none;
&.active { display: flex; }
font-size: 1rem;
line-height: 1rem;
padding: 0.5rem 0.75rem 0.5rem;
margin: 0.5rem 0 0;
background: themed("color-section-lighten-5");
border-radius: 0.25rem 0.25rem 0 0;
}
tab-entry {
display: none !important;
&.active { display: grid !important; }
&.flex.active { display: flex !important; }
grid-template-areas: "head" "body" "hint";
grid-template-columns: 1fr;
align-items: center;
gap: 0.25rem;
background: themed("color-section");
padding: 0.5rem;
tab-head {
grid-area: head;
margin-left: 0.25em;
font-size: 0.85rem;
label { font-weight: normal; }
}
tab-body {
grid-area: body;
}
tab-hint {
grid-area: hint;
font-size: 90%;
color: themed("color-text-muted");
}
&.inline {
grid-template-areas: "head body" "hint hint";
grid-template-columns: 2fr 1fr;
tab-body { justify-self: end; }
}
&.blocky {
grid-template-areas: "body";
grid-template-columns: 1fr;
}
&.bigtext {
grid-template-columns: 1fr;
grid-template-areas: "head" "body" "hint";
}
&.buttony tab-body {
display: grid;
grid-template-columns: 1fr 4rem;
input[type="text"] { border-radius: 0.25rem 0 0 0.25rem !important; }
input[disabled] { cursor: not-allowed; }
a.st-button {
border-radius: 0 0.25rem 0.25rem 0 !important;
justify-content: center;
}
}
}
}
tab-submit {
position: sticky;
bottom: 0;
background: themed("color-section");
border-radius: 0.25rem;
width: min-content;
margin-top: 0.5rem;
}
@include window-larger-than(50rem) {
tabs-menu {
input[name="search"] {
width: unset;
border-radius: 0.25rem 0.25rem 0.25rem 0;
border-bottom: 2px solid themed("color-section");
}
}
tabs-content {
tab-entry {
grid-template-areas: "head body" ". hint";
grid-template-columns: 1fr 1fr;
&.inline {
grid-template-areas: "head hint body";
grid-template-columns: 8fr 7fr 1fr;
}
}
}
}

View File

@ -0,0 +1,10 @@
body.c-users.a-show {
@import "partials/about";
@import "partials/ban_banner";
@import "partials/card";
@import "partials/post_summary";
@import "partials/profile_section";
@import "partials/staff_info";
@import "partials/user_info";
}

View File

@ -0,0 +1,44 @@
tabs-menu, tabs-content {
max-width: 100rem;
}
#profile-tabs[data-has-about="false"][data-has-artinfo="false"] {
display: none;
}
.profile-about-section, .profile-artinfo-section {
flex-flow: column;
align-items: start;
tab-head {
font-weight: bold;
font-size: 1rem;
}
}
@include window-larger-than(50rem) {
tabs-menu { display: none; }
tabs-content {
display: grid;
grid-template-columns: 15rem 1fr;
grid-template-rows: min-content min-content;
gap: 1rem;
}
.profile-user-info {
display: grid !important;
grid-template-areas: unset;
grid-row: 1 / 4;
height: min-content;
}
.profile-about-section {
display: flex !important;
grid-column: 2 / -1;
}
.profile-artinfo-section {
display: flex !important;
grid-column: 2 / -1;
}
}

View File

@ -0,0 +1,9 @@
.profile-ban {
background: var(--palette-background-red);
padding: 0.5rem;
@include st-radius;
.styled-dtext p:last-child {
margin-bottom: 0;
}
}

View File

@ -0,0 +1,87 @@
.profile-card {
display: grid;
grid-template-columns: min-content auto;
gap: 1rem;
// Info block
.profile-info {
display: flex;
flex-flow: column;
justify-content: center;
gap: 0.25rem;
}
.profile-name {
display: flex;
flex-flow: row;
gap: 0.5rem;
align-items: center;
a {
color: themed("color-text");
font-size: 1.25rem;
}
}
.profile-joined {
font-size: 0.75rem;
color: themed("color-text-muted");
margin-top: -0.25rem;
}
}
.profile-quickie {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
.entry {
display: flex;
flex-flow: column;
align-items: center;
}
.entry > a {
display: grid;
grid-template-columns: min-content 1fr;
grid-template-areas:
"icon header"
"icon title ";
column-gap: 0.25rem;
align-items: center;
width: 7rem;
padding: 0.5rem;
font-size: 1rem;
@include st-radius;
background: themed("color-section");
&:hover { background: themed("color-section-lighten-5"); }
.entry-icon {
grid-area: icon;
height: 1.5rem;
width: 1.5rem;
color: themed("color-link-active");
}
.entry-header {
grid-area: header;
font-size: 1.1rem;
line-height: 1.1rem;
white-space: nowrap;
}
.entry-title {
grid-area: title;
font-size: 0.9rem;
line-height: 0.9rem;
white-space: nowrap;
}
}
.entry-extra {
a, a:visited { color: themed("color-text-muted"); }
a:hover, a:active { color: themed("color-link-active"); }
}
}

View File

@ -0,0 +1,107 @@
.posts-section {
display: flex;
flex-flow: column;
gap: 1em;
// Hack to fix spacing on mobile
margin-top: 1rem;
@include window-larger-than(50rem) {
margin-top: 0;
}
.profile-sample {
display: grid;
grid-template: "p-header"
"p-links"
"p-posts";
gap: 0.5em 0;
@include window-larger-than(50rem) {
grid-template: "p-header p-links"
"p-posts p-posts";
grid-template-columns: 12em 1fr;
gap: 0 0.5em;
}
}
.profile-sample-header a, .profile-sample-links a {
display: block;
box-sizing: border-box;
align-content: center;
text-align: center;
height: 100%;
padding: 0.5em;
border-radius: 6px;
background-color: themed("color-section");
&:hover {
background-color: themed("color-section-lighten-5");
}
&:focus, &:active {
outline: 0;
color: themed("color-link-active");
}
}
.profile-sample-header {
grid-area: p-header;
display: flex;
a {
font-size: 1.25em;
font-weight: bold;
width: 100%;
}
@include window-larger-than(800px) {
a { border-radius: 6px 6px 0 0; }
}
}
.profile-sample-links {
grid-area: p-links;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
text-align: center;
span {
padding: 0.5em;
color: themed("color-text-muted");
}
.spacer { display: none; }
.profile-comments-link {
grid-row: 1 / 3;
grid-column: 3;
}
@include window-larger-than(50rem) {
display: flex;
a { height: min-content; }
.spacer { display: block; }
}
}
.profile-sample-posts {
grid-area: p-posts;
display: flex;
overflow: auto;
justify-content: center;
flex-wrap: wrap;
gap: 1em;
padding: 0.5em;
background: var(--color-section);
border-radius: 6px;
// Desktop
@include window-larger-than(50rem) {
flex-wrap: nowrap;
border-top-left-radius: 0;
justify-content: flex-start;
}
}
}

View File

@ -0,0 +1,35 @@
.profile-section {
background: themed("color-section");
.profile-section-header {
background: themed("color-section-lighten-5");
@include st-radius;
padding: 0.25rem 0.5rem;
cursor: pointer;
&::before {
content: "";
display: inline-flex;
transition: transform 200ms;
margin-right: 0.25rem;
}
}
.profile-section-body {
padding: 0.25rem 0.5rem;
border-radius: 0 0 radius() radius();
overflow: hidden;
}
// Collapsed
&.hidden {
.profile-section-header::before {
transform: rotate(-90deg);
}
.profile-section-body {
padding: 0 0.5rem;
height: 0;
}
}
}

View File

@ -0,0 +1,16 @@
.profile-staff-info {
display: grid;
grid-template-columns: min-content 1fr;
gap: 0.25rem 1rem;
h4 {
white-space: nowrap;
}
.block { grid-column: 1 / -1; }
h4.block { margin-bottom: -0.25rem; }
@include window-larger-than(50rem) {
grid-template-columns: min-content 1fr min-content 1fr;
}
}

View File

@ -0,0 +1,71 @@
.profile-user-info {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(auto-fit, minmax(min-content, 5rem));
background: themed("color-section");
@include st-radius;
@include window-larger-than(38rem) { grid-template-columns: 1fr 1fr; }
@include window-larger-than(50rem) { grid-template-columns: 1fr; }
.profile-line {
display: grid;
grid-template-columns: 1fr min-content;
padding: 1rem 0.5rem;
border-bottom: 2px solid themed("color-foreground");
h4 {
// Not necessary, but it aligns
// with the -extra line better
display: flex;
align-items: center;
gap: 0.25em;
svg {
width: 1em;
height: 1em;
vertical-align: middle;
}
}
&-number {
white-space: nowrap;
}
&-extra {
grid-column: 1 / -1;
font-size: 90%;
color: themed("color-text-muted");
margin-left: 1.25em;
}
// Stats display section
&-show {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 2rem;
margin-top: 0.5rem;
.entry {
text-align: center;
width: 5rem;
& > span { color: themed("color-text-muted"); }
}
}
// Dot-separated, less important
&-list {
display: flex;
gap: 0.25em;
a:not(:last-child)::after {
content: "";
margin-left: 0.25rem;
color: themed("color-text-muted");
}
}
&:last-child { border-bottom: none; }
}
}

View File

@ -487,6 +487,8 @@ class Artist < ApplicationRecord
if params[:is_linked].to_s.truthy? if params[:is_linked].to_s.truthy?
q = q.where("linked_user_id IS NOT NULL") q = q.where("linked_user_id IS NOT NULL")
elsif params[:is_linked].to_s.falsy?
q = q.where("linked_user_id IS NULL")
end end
case params[:order] case params[:order]

View File

@ -80,10 +80,14 @@ class Tag < ApplicationRecord
def category_for(tag_name) def category_for(tag_name)
Cache.fetch("tc:#{tag_name}") do Cache.fetch("tc:#{tag_name}") do
Tag.where(name: tag_name).pick(:category).to_i category_for!(tag_name).to_i
end end
end end
def category_for!(tag_name)
Tag.where(name: tag_name).pick(:category)
end
def categories_for(tag_names, disable_cache: false) def categories_for(tag_names, disable_cache: false)
if disable_cache if disable_cache
tag_cats = {} tag_cats = {}

View File

@ -574,6 +574,11 @@ class User < ApplicationRecord
base_upload_limit + (pieces[:approved] / 10) - (pieces[:deleted] / 4) - pieces[:pending] base_upload_limit + (pieces[:approved] / 10) - (pieces[:deleted] / 4) - pieces[:pending]
end end
def upload_limit_max
pieces = upload_limit_pieces
base_upload_limit + (pieces[:approved] / 10) - (pieces[:deleted] / 4)
end
def upload_limit_pieces def upload_limit_pieces
@upload_limit_pieces ||= begin @upload_limit_pieces ||= begin
deleted_count = Post.deleted.for_user(id).count deleted_count = Post.deleted.for_user(id).count
@ -739,6 +744,28 @@ class User < ApplicationRecord
user_status.ticket_count user_status.ticket_count
end end
def feedback_pieces
@feedback_pieces ||= begin
count = {
deleted: 0,
negative: 0,
neutral: 0,
positive: 0,
}
feedback.each do |one|
if one.is_deleted
count[:deleted] += 1
next
end
count[one.category.to_sym] += 1
end
count
end
end
def positive_feedback_count def positive_feedback_count
feedback.active.positive.count feedback.active.positive.count
end end

View File

@ -1,14 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
class WikiPage < ApplicationRecord class WikiPage < ApplicationRecord
class RevertError < Exception ; end class RevertError < Exception; end
before_validation :normalize_title before_validation :normalize_title
before_validation :normalize_other_names before_validation :normalize_other_names
before_validation :normalize_parent before_validation :normalize_parent
before_save :log_changes
before_save :update_tag, if: :tag_changed?
before_destroy :validate_not_used_as_help_page
before_destroy :log_destroy
after_save :create_version after_save :create_version
after_save :update_help_page, if: :saved_change_to_title?
normalizes :body, with: ->(body) { body.gsub("\r\n", "\n") } normalizes :body, with: ->(body) { body.gsub("\r\n", "\n") }
validates :title, uniqueness: { :case_sensitive => false }
validates :title, uniqueness: { case_sensitive: false }
validates :title, presence: true validates :title, presence: true
validates :title, tag_name: true, if: :title_changed? validates :title, tag_name: true, if: :title_changed?
validates :body, presence: { unless: -> { is_deleted? || other_names.present? || parent.present? } } validates :body, presence: { unless: -> { is_deleted? || other_names.present? || parent.present? } }
@ -19,12 +26,8 @@ class WikiPage < ApplicationRecord
validate :validate_redirect validate :validate_redirect
validate :validate_not_locked validate :validate_not_locked
before_destroy :validate_not_used_as_help_page
before_destroy :log_destroy
before_save :log_changes
after_save :update_help_page, if: :saved_change_to_title?
attr_accessor :skip_secondary_validations, :edit_reason attr_accessor :skip_secondary_validations, :edit_reason
array_attribute :other_names array_attribute :other_names
belongs_to_creator belongs_to_creator
belongs_to_updater belongs_to_updater
@ -33,19 +36,19 @@ class WikiPage < ApplicationRecord
has_many :versions, -> { order("wiki_page_versions.id ASC") }, class_name: "WikiPageVersion", dependent: :destroy has_many :versions, -> { order("wiki_page_versions.id ASC") }, class_name: "WikiPageVersion", dependent: :destroy
has_one :help_page, foreign_key: "wiki_page", primary_key: "title" has_one :help_page, foreign_key: "wiki_page", primary_key: "title"
def log_destroy
ModAction.log(:wiki_page_delete, {wiki_page: title, wiki_page_id: id})
end
def log_changes def log_changes
if title_changed? && !new_record? if title_changed? && !new_record?
ModAction.log(:wiki_page_rename, {new_title: title, old_title: title_was}) ModAction.log(:wiki_page_rename, { new_title: title, old_title: title_was })
end end
if is_locked_changed? if is_locked_changed?
ModAction.log(is_locked ? :wiki_page_lock : :wiki_page_unlock, {wiki_page: title}) ModAction.log(is_locked ? :wiki_page_lock : :wiki_page_unlock, { wiki_page: title })
end end
end end
def log_destroy
ModAction.log(:wiki_page_delete, { wiki_page: title, wiki_page_id: id })
end
module SearchMethods module SearchMethods
def titled(title) def titled(title)
find_by(title: WikiPage.normalize_name(title)) find_by(title: WikiPage.normalize_name(title))
@ -123,7 +126,7 @@ class WikiPage < ApplicationRecord
module ApiMethods module ApiMethods
def method_attributes def method_attributes
super + [:creator_name, :category_id] super + %i[creator_name category_id]
end end
end end
@ -140,9 +143,92 @@ class WikiPage < ApplicationRecord
end end
end end
module TagMethods
def tag
@tag ||= super
end
def category_id
return @category_id if instance_variable_defined?(:@category_id)
@category_id = tag&.category
end
def category_id=(value)
return if value.blank? || value.to_i == category_id
category_id_will_change!
@category_id = value.to_i
end
def category_is_locked
return @category_is_locked if instance_variable_defined?(:@category_is_locked)
@category_is_locked = tag&.is_locked || false
end
def category_is_locked=(value)
return if value == category_is_locked
category_is_locked_will_change!
@category_is_locked = value
end
def category_id_changed?
attribute_changed?("category_id")
end
def category_id_will_change!
attribute_will_change!("category_id")
end
def category_is_locked_changed?
attribute_changed?("category_is_locked")
end
def category_is_locked_will_change!
attribute_will_change!("category_is_locked")
end
def tag_update_map
{}.tap do |updates|
updates[:category] = @category_id if category_id_changed?
updates[:is_locked] = @category_is_locked if category_is_locked_changed?
end
end
def tag_changed?
tag_update_map.present?
end
def update_tag
updates = tag_update_map
@tag = Tag.find_or_create_by_name(title)
return if updates.empty?
unless @tag.category_editable_by?(CurrentUser.user)
reload_tag_attributes
errors.add(:category_id, "Cannot be changed")
throw(:abort)
end
@tag.update(updates)
@tag.save
if @tag.invalid?
errors.add(:category_id, @tag.errors.full_messages.join(", "))
throw(:abort)
end
reload_tag_attributes
end
def reload_tag_attributes
remove_instance_variable(:@category_id) if instance_variable_defined?(:@category_id)
remove_instance_variable(:@category_is_locked) if instance_variable_defined?(:@category_is_locked)
end
end
extend SearchMethods extend SearchMethods
include ApiMethods include ApiMethods
include HelpPageMethods include HelpPageMethods
include TagMethods
def user_not_limited def user_not_limited
allowed = CurrentUser.can_wiki_edit_with_reason allowed = CurrentUser.can_wiki_edit_with_reason
@ -156,7 +242,7 @@ class WikiPage < ApplicationRecord
def validate_not_locked def validate_not_locked
if is_locked? && !CurrentUser.is_janitor? if is_locked? && !CurrentUser.is_janitor?
errors.add(:is_locked, "and cannot be updated") errors.add(:is_locked, "and cannot be updated")
return false false
end end
end end
@ -203,7 +289,12 @@ class WikiPage < ApplicationRecord
end end
def normalize_title def normalize_title
self.title = title.downcase.tr(" ", "_") title = self.title.downcase.tr(" ", "_")
if title =~ /\A(#{Tag.categories.regexp}):(.+)\Z/
self.category_id = Tag.categories.value_for($1)
title = $2
end
self.title = title
end end
def normalize_other_names def normalize_other_names
@ -226,16 +317,12 @@ class WikiPage < ApplicationRecord
@skip_secondary_validations = value.to_s.truthy? @skip_secondary_validations = value.to_s.truthy?
end end
def category_id
Tag.category_for(title)
end
def pretty_title def pretty_title
title&.tr("_", " ") || '' title&.tr("_", " ") || ""
end end
def pretty_title_with_category def pretty_title_with_category
return pretty_title if category_id == 0 return pretty_title if category_id.blank? || category_id == 0
"#{Tag.category_for_value(category_id)}: #{pretty_title}" "#{Tag.category_for_value(category_id)}: #{pretty_title}"
end end
@ -274,10 +361,6 @@ class WikiPage < ApplicationRecord
else else
match match
end end
end.map {|x| x.downcase.tr(" ", "_").to_s}.uniq end.map { |x| x.downcase.tr(" ", "_").to_s }.uniq
end
def visible?
true
end end
end end

View File

@ -6,7 +6,6 @@ class WikiPageVersion < ApplicationRecord
belongs_to_updater belongs_to_updater
user_status_counter :wiki_edit_count, foreign_key: :updater_id user_status_counter :wiki_edit_count, foreign_key: :updater_id
belongs_to :artist, optional: true belongs_to :artist, optional: true
delegate :visible?, to: :wiki_page
module SearchMethods module SearchMethods
def for_user(user_id) def for_user(user_id)

View File

@ -53,6 +53,11 @@ class UserPresenter
= <abbr title="User Upload Limit Remaining">#{user.upload_limit}</abbr>}.html_safe = <abbr title="User Upload Limit Remaining">#{user.upload_limit}</abbr>}.html_safe
end end
def upload_limit_short
return "none" if user.can_upload_free?
"#{user.upload_limit} / #{user.upload_limit_max}"
end
def uploads def uploads
posts = Post.tag_match("user:#{user.name}").limit(8) posts = Post.tag_match("user:#{user.name}").limit(8)
PostsDecorator.decorate_collection(posts) PostsDecorator.decorate_collection(posts)

View File

@ -0,0 +1,34 @@
<a
href="<%= user_feedbacks_path(search: { user_id: user.id }) %>"
class="user-records-list"
data-negative="<%= negative %>"
data-neutral="<%= negative %>"
data-positive="<%= neutral %>"
data-total="<%= positive - negative %>"
>
<% if deleted > 0 %>
<span class="user-record deleted"><%= deleted %></span>
<% end %>
<% if negative > 0 %>
<span class="user-record negative"><%= negative %></span>
<% end %>
<% if neutral > 0 %>
<span class="user-record neutral"><%= neutral %></span>
<% end %>
<% if positive > 0 %>
<span class="user-record positive"><%= positive %></span>
<% end %>
</a>
<% if CurrentUser.is_moderator? && CurrentUser.user != user && active == 0 %>
<a
href="<%= new_user_feedback_path(user_feedback: { user_id: user.id, category: "neutral" }) %>"
class="user-records-list"
title="New Feedback"
>
<span class="user-record neutral"><%= svg_icon(:plus) %></span>
</a>
<% end %>

View File

@ -0,0 +1,11 @@
<a
href="<%= user_path(user) %>"
class="profile-avatar placeholder<%= klass.nil? ? "" : klass%>"
data-id="<%= post_id %>"
data-name="<%= user.name %>"
>
<span
class="avatar-image"
data-initial="<%= user.name[0].capitalize%>"
></span>
</a>

View File

@ -3,6 +3,6 @@
<%= f.input :url_matches, label: "URL", as: :string %> <%= f.input :url_matches, label: "URL", as: :string %>
<%= f.user :creator %> <%= f.user :creator %>
<%= f.input :has_tag, label: "Has tag?", collection: [["Yes", true], ["No", false]], include_blank: true %> <%= f.input :has_tag, label: "Has tag?", collection: [["Yes", true], ["No", false]], include_blank: true %>
<%= f.input :is_linked, label: "Linked Only", as: :boolean %> <%= f.input :is_linked, label: "Linked?", collection: [["Yes", true], ["No", false]], include_blank: true %>
<%= f.input :order, collection: [["Recently created", "created_at"], ["Last updated", "updated_at"], ["Name", "name"], ["Post count", "post_count"]] %> <%= f.input :order, collection: [["Recently created", "created_at"], ["Last updated", "updated_at"], ["Name", "name"], ["Post count", "post_count"]] %>
<% end %> <% end %>

View File

@ -1,3 +1,9 @@
<% if CurrentUser.is_anonymous? %>
<%= decorated_nav_link_to("Log In", :log_in, new_session_path, class: "nav-hidden") %>
<% else %>
<%= decorated_nav_link_to("Profile", :user, user_path(CurrentUser.user), class: "nav-hidden") %>
<% end %>
<%= decorated_nav_link_to("Artists", :brush, artists_path) %>
<%= decorated_nav_link_to("Posts", :images, posts_path) %> <%= decorated_nav_link_to("Posts", :images, posts_path) %>
<%= decorated_nav_link_to("Pools", :library, gallery_pools_path) %> <%= decorated_nav_link_to("Pools", :library, gallery_pools_path) %>
<%= decorated_nav_link_to("Sets", :group, post_sets_path) %> <%= decorated_nav_link_to("Sets", :group, post_sets_path) %>

View File

@ -20,11 +20,11 @@
</a> </a>
<% if CurrentUser.is_anonymous? %> <% if CurrentUser.is_anonymous? %>
<a href="<%= new_session_path %>" class="simple-avatar nav-controls-profile collapse-2"> <a href="<%= new_session_path %>" class="simple-avatar nav-controls-profile collapse-2">
<span class="simple-avatar-button sign-in"> <span class="avatar-button sign-in">
<span class="simple-avatar-name"> <span class="avatar-name">
Sign In Sign In
</span> </span>
<span class="simple-avatar-image"> <span class="avatar-image">
<%= svg_icon(:log_in) %> <%= svg_icon(:log_in) %>
</span> </span>
</span> </span>

View File

@ -8,10 +8,12 @@
"th-sheader": localStorage.getItem("theme-sheader") || false, "th-sheader": localStorage.getItem("theme-sheader") || false,
"th-forumnotif": localStorage.getItem("theme-forumnotif") || false, "th-forumnotif": localStorage.getItem("theme-forumnotif") || false,
"th-palette": localStorage.getItem("theme-palette") || "default", "th-palette": localStorage.getItem("theme-palette") || "default",
"th-font": localStorage.getItem("theme-font") || "Verdana",
"th-nav": localStorage.getItem("theme-nav") || "top", "th-nav": localStorage.getItem("theme-nav") || "top",
// Settings // Settings
"st-fullscreen": localStorage.getItem("e6.posts.fusk") || false, "st-fullscreen": localStorage.getItem("e6.posts.fusk") || false,
"st-ssearch": localStorage.getItem("e6.posts.ssearch") || false,
}; };
var b = document.body; var b = document.body;

View File

@ -24,6 +24,7 @@
<div class="content"> <div class="content">
<%= render "ads/leaderboard", tag_string: @post_set.ad_tag_string %> <%= render "ads/leaderboard", tag_string: @post_set.ad_tag_string %>
<%= render "posts/partials/index/edit" %> <%= render "posts/partials/index/edit" %>
<%= render "posts/partials/index/stats", :post_set => @post_set %>
<%= render "posts/partials/index/posts", :post_set => @post_set %> <%= render "posts/partials/index/posts", :post_set => @post_set %>
</div> </div>

View File

@ -10,6 +10,6 @@
<% end %> <% end %>
<%= text_area_tag("tags", tags, placeholder: "Search posts by tag (title:\"video name\")", rows: 1, data: { shortcut: "q", autocomplete: "tag-query" }) %> <%= text_area_tag("tags", tags, placeholder: "Search posts by tag (title:\"video name\")", rows: 1, data: { shortcut: "q", autocomplete: "tag-query" }) %>
<%= tag.button(tag.i(class: "fa-solid fa-magnifying-glass"), type: "submit", title: "Search") %> <%= tag.button(svg_icon(:search), type: "submit", title: "Search") %>
<% end %> <% end %>
</section> </section>

View File

@ -1,6 +1,8 @@
<div class="search-controls"> <div class="search-controls">
<button id="search-fullscreen" class="st-button w100 stealth"> <button id="search-fullscreen" class="st-button" title="Fullscreen mode">
<%= svg_icon(:fullscreen) %> <%= svg_icon(:fullscreen) %>
<span>Fullscreen</span> </button>
<button id="search-sticky" class="st-button" title="Sticky searchbar">
<%= svg_icon(:anchor) %>
</button> </button>
</div> </div>

View File

@ -0,0 +1,3 @@
<div class="posts-index-stats">
<%= approximate_count(post_set.posts) %>
</div>

View File

@ -1,15 +1,12 @@
<% if CurrentUser.can_view_staff_notes? %> <% if CurrentUser.can_view_staff_notes? %>
<div class="staff-notes-section styled-dtext"> <div class="profile-section hidden" name="StaffNotes">
<details> <div class="profile-section-header">Staff Notes (<%= user.staff_notes.count %>)</div>
<summary>Staff Notes (<%= user.staff_notes.count %>)</summary> <div class="profile-section-body styled-dtext">
<div> <%= render "staff_notes/partials/list_of_notes", staff_notes: user.staff_notes.limit(15), show_receiver_name: false %>
<h4><%= link_to "Staff Notes", staff_notes_path(search: { user_id: user.id }) %></h4> <div class="new-staff-note">
<%= render "staff_notes/partials/list_of_notes", staff_notes: user.staff_notes.limit(15), show_receiver_name: false %> <p><%= link_to "Create »", new_staff_note_path(search: { user_id: user.id }), class: "expand-new-staff-note" %></p>
<div class="new-staff-note"> <%= render "staff_notes/partials/new", user: user, staff_note: StaffNote.new(user_id: user.id), hidden: true %>
<p><%= link_to "Create »", new_staff_note_path(search: { user_id: user.id }), class: "expand-new-staff-note" %></p> </div>
<%= render "staff_notes/partials/new", user: user, staff_note: StaffNote.new(user_id: user.id), hidden: true %>
</div>
</div>
</details>
</div> </div>
</div>
<% end %> <% end %>

View File

@ -10,7 +10,7 @@
<!-- Primary Searchbar --> <!-- Primary Searchbar -->
<div class="home-search"> <div class="home-search">
<%= text_field_tag("tags", "", autofocus: "autofocus", placeholder: "Search posts by tag (title:\"video name\")", data: { shortcut: "q", autocomplete: "tag-query" }) %> <%= text_field_tag("tags", "", autofocus: "autofocus", placeholder: "Search posts by tag (title:\"video name\")", data: { shortcut: "q", autocomplete: "tag-query" }) %>
<%= tag.button(tag.i(class: "fa-solid fa-magnifying-glass"), type: "submit") %> <%= tag.button(svg_icon(:search), type: "submit") %>
</div> </div>
<!-- Secondary search buttons --> <!-- Secondary search buttons -->

View File

@ -16,8 +16,8 @@
This means that they will not persist across multiple devices, or in incognito mode. This means that they will not persist across multiple devices, or in incognito mode.
</p> </p>
<label for="theme_main">Theme</label> <label for="Theme_Main">Theme</label>
<select id="theme_main"> <select id="Theme_Main">
<option value="hexagon">Hexagon</option> <option value="hexagon">Hexagon</option>
<option value="bloodlust">Bloodlust</option> <option value="bloodlust">Bloodlust</option>
<option value="pony">Pony</option> <option value="pony">Pony</option>
@ -25,8 +25,8 @@
<option value="hotdog">Hotdog</option> <option value="hotdog">Hotdog</option>
</select> </select>
<label for="theme_extra">Embellishments</label> <label for="Theme_Extra">Embellishments</label>
<select id="theme_extra"> <select id="Theme_Extra">
<option value="none">(None)</option> <option value="none">(None)</option>
<option value="aurora">Aurora</option> <option value="aurora">Aurora</option>
<option value="autumn">Autumn</option> <option value="autumn">Autumn</option>
@ -37,42 +37,68 @@
<option value="winter">Eternal Winter</option> <option value="winter">Eternal Winter</option>
</select> </select>
<label for="theme_stickyheader">Sticky Header</label>
<select id="theme_stickyheader">
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
<label for="theme_forumnotif">Forum Activity Dot</label>
<select id="theme_forumnotif">
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
<h3>Accessibility</h3> <h3>Accessibility</h3>
<label for="theme_palette">Palette</label> <label for="Theme_Palette">Palette</label>
<select id="theme_palette"> <select id="Theme_Palette">
<option value="default">Default</option> <option value="default">Default</option>
<option value="deut">Protanopia & Deuteranopia</option> <option value="deut">Protanopia & Deuteranopia</option>
<option value="trit">Tritanopia</option> <option value="trit">Tritanopia</option>
</select> </select>
<label for="Theme_Font">Font</label>
<select id="Theme_Font">
<option value="Verdana">Verdana</option>
<option value="Lato">Lato</option>
<option value="Lexend">Lexend</option>
<option value="Monospace">Monospace</option>
<option value="OpenDyslexic">Open Dyslexic</option>
<option value="OpenSans">Open Sans</option>
<option value="ComicSans">Comic Sans</option>
</select>
<h3>Features</h3>
<label for="Theme_ForumNotif">Forum Activity Dot</label>
<select id="Theme_ForumNotif">
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
<label for="Posts_WikiExcerpt">Wiki Excerpt</label>
<select id="Posts_WikiExcerpt">
<option value="0">Collapsed</option>
<option value="1">Expanded</option>
<option value="2">Disabled</option>
</select>
<h3>Navigation</h3> <h3>Navigation</h3>
<label for="theme_navbar">Navbar location</label> <label for="Theme_StickyHeader">Sticky Header</label>
<select id="theme_navbar"> <select id="Theme_StickyHeader">
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
<label for="Posts_StickySearch">Sticky Searchbar</label>
<select id="Posts_StickySearch">
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
<label for="Theme_Navbar">Navbar location</label>
<select id="Theme_Navbar">
<option value="top">Top (default)</option> <option value="top">Top (default)</option>
<option value="bottom">Bottom</option> <option value="bottom">Bottom</option>
<option value="both">Both</option> <option value="both">Both</option>
<option value="none">Off (not recommended)</option> <option value="none">Off (not recommended)</option>
</select> </select>
<label for="theme_gestures">Mobile Gestures</label> <label for="Theme_Gestures">Mobile Gestures</label>
<select id="theme_gestures"> <select id="Theme_Gestures">
<option value="false">Disabled</option> <option value="false">Disabled</option>
<option value="true">Enabled</option> <option value="true">Enabled</option>
</select> </select>
<div class="hint">Swipe left for next page/image. Swipe right for previous page/image.</div> <div class="hint">Swipe left for next page/image. Swipe right for previous page/image.</div>
</div> </div>
@ -85,14 +111,6 @@
<%= svg_icon(:reset) %> <%= svg_icon(:reset) %>
Reset Reset
</button> </button>
<label for="wiki-excerpt-reset">Wiki Excerpt</label>
<span id="wiki-excerpt-value"></span>
<button class="st-button" id="wiki-excerpt-reset">
<%= svg_icon(:reset) %>
Reset
</button>
</div> </div>
<% content_for(:page_title) do %> <% content_for(:page_title) do %>

View File

@ -1,14 +0,0 @@
<div class="about-section">
<% if user.profile_about.present? %>
<div class="profile-about-entry" id="about-info">
<h3>About</h3>
<div class="content dtext-container"><%= format_text(user.profile_about, allow_color: true) %></div>
</div>
<% end %>
<% if user.profile_artinfo.present? %>
<div class="profile-about-entry" id="about-artinfo">
<h3>Artist Information</h3>
<div class="content dtext-container"><%= format_text(user.profile_artinfo, allow_color: true) %></div>
</div>
<% end %>
</div>

View File

@ -1,156 +0,0 @@
<div class="stats-section">
<div class="profile-avatar">
<%= user_avatar @user %>
</div>
<div class="profile-stats">
<h1><%= link_to_user @user %></h1>
<div class="user-statistics">
<div class="column">
<span>Join Date</span>
<span><%= compact_time @user.created_at %></span>
<span>Level</span>
<span><%= "(Unactivated)" unless user.is_verified? %> <%= presenter.level %></span>
<% if user.is_banned? && user.recent_ban %>
<span>Ban reason</span>
<span class="dtext-container"><%= format_text presenter.ban_reason %></span>
<% end %>
<span>Posts</span>
<span>
<%= presenter.active_upload_count(self) %>
[<%= link_to "pending", posts_path(tags: "user:#{user.name} status:pending") %>]
(<%= link_to "comments on", comments_path(group_by: :comment, search: {poster_id: user.id}) %>)
<% if CurrentUser.is_moderator? %>
(<%= link_to "votes", action: "index", controller: "post_votes", search: { user_name: user.name } %>)
<% end %>
</span>
<span>Deleted</span>
<span>
<%= presenter.deleted_upload_count(self) %>
</span>
<span>Replaced</span>
<span>
<%= presenter.replaced_upload_count(self) %>
[<%= link_to "pending", post_replacements_path(search: { creator_name: user.name }) %>]
</span>
<span>Rejected</span>
<span><%= presenter.rejected_replacements_count(self) %></span>
<span>Favorites</span>
<span>
<%= presenter.favorite_count(self) %>
</span>
<span>Forum Posts</span>
<span>
<%= presenter.forum_post_count(self) %>
(<%= link_to "mentions", forum_posts_path(search: { body_matches: user.name }) %>)
</span>
<span>Comments</span>
<span>
<%= presenter.comment_count(self) %> on <%= presenter.commented_posts_count(self) %> posts
(<%= link_to "mentions", comments_path(group_by: :comment, search:{ body_matches: user.name }) %>)
<% if CurrentUser.is_moderator? %>
(<%= link_to "votes", action: "index", controller: "comment_votes", search: { user_name: user.name } %>)
<% end %>
</span>
<% if user.can_approve_posts? || Post.where(approver: user).exists? %>
<span>Approvals</span>
<span><%= presenter.approval_count(self) %></span>
<% end %>
<% if CurrentUser.user.id == user.id || CurrentUser.is_janitor? %>
<% if presenter.previous_names(self).present? %>
<span>Previous Names</span>
<span><%= presenter.previous_names(self) %> -> <%= user.name %></span>
<% end %>
<% end %>
<% if CurrentUser.is_admin? %>
<span>Email</span>
<span>
<%= user.email %>
<%= email_domain_search(user.email) %>
</span>
<span>Last IP</span>
<span><%= link_to_ip(user.last_ip_addr) %></span>
<% end %>
</div>
<div class="column">
<span>Feedback</span>
<span>
<%= presenter.feedbacks %>
<%= link_to("List", user_feedbacks_path(search: { user_id: @user.id })) %>
<% if CurrentUser.is_moderator? && @user.feedback.active.count == 0 %>
| <%= link_to("Create", new_user_feedback_path(user_feedback: { user_id: @user.id, category: "neutral" })) %>
<% end %>
</span>
<span>Permissions</span>
<span><%= presenter.permissions %></span>
<span>Upload Limit</span>
<span>
<%= presenter.upload_limit(self) %>
<% if CurrentUser.user.id == user.id %>
(<%= link_to "help", upload_limit_users_path %>)
<% else %>
(<%= link_to "help", wiki_page_path(id: "upload_limit") %>)
<% end %>
</span>
<span>Post Changes</span>
<span>
<%= presenter.post_version_count(self) %>
<% if CurrentUser.is_moderator? && UserRevert.can_revert?(user) %>
[<%= link_to "revert all", new_user_revert_path(user_id: user.id) %>]
<% end %>
</span>
<span>Wiki Changes</span>
<span><%= presenter.wiki_page_version_count(self) %></span>
<span>Note Changes</span>
<span><%= presenter.note_version_count(self) %> on <%= presenter.noted_posts_count(self) %> posts</span>
<span>Artist Changes</span>
<span><%= presenter.artist_version_count(self) %></span>
<span>Pool Changes</span>
<span><%= presenter.pool_version_count(self) %></span>
<% if CurrentUser.user.id == user.id || CurrentUser.is_janitor? %>
<span>Flags</span>
<span><%= presenter.flag_count(self) %></span>
<% end %>
<% if CurrentUser.user.id == user.id || CurrentUser.is_moderator? %>
<span>Tickets</span>
<span>
<%= presenter.ticket_count(self) %>
<% if CurrentUser.is_moderator? %>
[<%= link_to "pending", tickets_path(search: { creator_id: user.id, status: "pending" }) %>]
[<%= link_to "accused", tickets_path(search: { accused_id: user.id }) %>]
<% end %>
</span>
<% end %>
<% if CurrentUser.id == user.id %>
<span>API Key</span>
<span>
<%= link_to (CurrentUser.api_key ? "View" : "Generate"), user_api_key_path(CurrentUser.user) %>
(<%= link_to "help", help_page_path(id: "api") %>)
</span>
<% end %>
</div>
</div>
</div>
</div>

View File

@ -1,137 +1,36 @@
<div id="c-users"> <div id="c-users"><div id="a-edit">
<div id="a-edit"> <h1>Settings</h1>
<h1>Settings</h1>
<%= custom_form_for @user do |f| %> <tabs-menu id="settings-tabs">
<h2 id="edit-options"> <button role="tab" name="basic">Basic</button>
<%= link_to "Basic", "#basic-settings", :class => "active" %> <button role="tab" name="advanced">Advanced</button>
| <%= link_to "Advanced", "#advanced-settings" %> <button role="tab" name="blacklist">Blacklist</button>
<% if CurrentUser.user.id == @user.id %> <span class="spacer"></span>
| <%= link_to "Change password", edit_user_password_path(:user_id => @user.id), :id => "change-password" %> <input type="text" name="search" placeholder="Search Settings" />
| <%= link_to "Delete account", maintenance_user_deletion_path, :id => "delete-account" %> </tabs-menu>
<% end %>
</h2>
<fieldset id="basic-settings-section"> <%= custom_form_for @user do |form| %>
<div class="input"> <tabs-content for="settings-tabs">
<label>Name</label> <%= render "users/partials/edit/basic", form: form %>
<%= render "users/partials/edit/advanced", form: form %>
<%= render "users/partials/edit/blacklist", form: form %>
<tab-submit>
<%= form.button :submit, "Save Settings" %>
</tab-submit>
</tabs-content>
<% end %>
</div></div>
<p><%= link_to "Request a name change", new_user_name_change_request_path %></p> <noscript>
</div> <style>
tabs-menu { display: none !important; }
<div class="input"> tab-group { display: flex !important; }
<label>Email</label> tab-entry { display: grid !important; }
<p> </style>
<% if CurrentUser.user.email.present? %> </noscript>
<%= CurrentUser.user.email %>
<% else %>
<em>blank</em>
<% end %>
<%= link_to "Change your email", new_maintenance_user_email_change_path %>
</p>
</div>
<%= f.input :avatar_id, as: :string, label: "Avatar Post ID" %>
<%= f.input :profile_about, as: :dtext, label: "About Me", limit: Danbooru.config.user_about_max_size, allow_color: true %>
<%= f.input :profile_artinfo, as: :dtext, label: "Commission Info", limit: Danbooru.config.user_about_max_size, allow_color: true %>
<%= f.input :time_zone, :include_blank => false %>
<%= f.input :receive_email_notifications, :as => :select, :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %>
<%= f.input :comment_threshold, :hint => "Comments below this score will be hidden by default." %>
<%= f.input :default_image_size, :hint => "Show original image size, scaled to fit, scaled to fit vertically, or show resized #{Danbooru.config.large_image_width} pixel sample version.", :label => "Default image width", :collection => [["Original", "original"], ["Fit (Horizontal)", "fit"], ["Fit (Vertical)", "fitv"], ["Sample (#{Danbooru.config.large_image_width}px)", "large"]], :include_blank => false %>
<%= f.input :per_page, :label => "Posts per page", :as => :select, :collection => (25..250), :include_blank => false %>
<%= f.input :enable_safe_mode, :label => "Safe mode", :hint => "Show only safe images. Hide questionable and explicit images.", :as => :select, :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %>
<%= f.input :blacklisted_tags, :hint => "Put any tag combinations you never want to see here. Each combination should go on a separate line. <a href='/help/blacklist'>View help.</a>".html_safe, autocomplete: "tag-query", input_html: { size: "40x5" } %>
<%= f.input :blacklist_users, hint: "Hide comments, blips and forum posts from users that have been blacklisted, in addition to posts.", as: :select, include_blank: false, collection: [["Yes", "true"], ["No", "false"]] %>
</fieldset>
<fieldset id="advanced-settings-section">
<%= f.input :style_usernames, :as => :select, :label => "Colored usernames",
:hint => raw("Color each user's name depending on their level. See #{link_to 'the legend', wiki_page_path(id: 'e621:colored_usernames')} for what the colors are."),
:include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %>
<%= f.input :enable_keyboard_navigation, :as => :select, :include_blank => false, :label => "Enable keyboard shortcuts", :collection => [["Yes", "true"], ["No", "false"]],
hint: raw("Enables the use of keyboard shortcuts for a majority of site actions related to posts. A list of keyboard shortcuts is available #{link_to 'here', keyboard_shortcuts_path}.")%>
<%= f.input :enable_auto_complete, :as => :select, :collection => [["Yes", "true"], ["No", "false"]], :include_blank => false,
hint: "Enables auto-completion on most tag and user entry fields." %>
<%= f.input :enable_privacy_mode, :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false,
hint: "Prevent showing your favorites to others users (except staff)." %>
<%= f.input :show_post_statistics, as: :select, collection: [["No", "false"],["Yes", "true"]], include_blank: false,
hint: "Show post statistics below posts on search pages." %>
<%= f.input :description_collapsed_initially, as: :select, collection: [["No", "false"],["Yes", "true"]], include_blank: false,
hint: "Don't expand post descriptions on page load." %>
<%= f.input :hide_comments, as: :select, collection: [["No", "false"],["Yes", "true"]], include_blank: false,
hint: "Do not show the comments section on post pages." %>
<% unless CurrentUser.is_janitor? %>
<%= f.input :disable_user_dmails, label: "Disable DMails", hint: "Prevent other users from sending you DMails. You will be prevented from sending DMails to non-staff members while this option is enabled. Staff are always allowed to send you DMails.",
:as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
<% end %>
<%= f.input :disable_cropped_thumbnails, :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false,
hint: "Disables displaying cropped thumbnails on the mobile layout of the site in favor of scaled thumbnails. Has no effect on the desktop site." %>
<%= f.input :show_hidden_comments, label: "Show Own Hidden Comments", :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false,
hint: "Show your own hidden comments on comment pages." %>
<% if @user.post_upload_count >= 10 %>
<%= f.input :enable_compact_uploader, label: "Enable Compact Uploader", as: :select,
collection: [["No", "false"], ["Yes", "true"]], include_blank: false,
hint: "Enables a more compact and less guided post uploader." %>
<% end %>
<div class="input text optional field_with_hint">
<label class="text optional" for="user_dmail_filter_attributes_words">Dmail filter</label>
<%= hidden_field_tag "user[dmail_filter_attributes][id]", @user.dmail_filter.try(:id) %>
<%= text_field_tag "user[dmail_filter_attributes][words]", @user.dmail_filter.try(:words), :id => "user_dmail_filter_attributes_words", :class => "text optional", :size => 40 %>
<span class="hint">A list of banned words (space delimited). Any dmail you receive with a banned word will automatically be deleted.</span>
</div>
<%= f.input :favorite_tags, :label => "Frequent tags", :hint => "A list of tags that you use often. They will appear when using the list of Related Tags.", autocomplete: "tag-query", input_html: { rows: 5 } %>
<%= f.input :disable_responsive_mode, :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false, :hint => "Disable alternative layout for mobile and tablet." %>
<%= f.input :custom_style, :label => "Custom <a href='https://en.wikipedia.org/wiki/Cascading_Style_Sheets'>CSS</a> style".html_safe, :hint => "Style to apply to the whole site.", :input_html => {:size => "40x5"} %>
</fieldset>
<%= f.button :submit, "Submit" %>
<% end %>
</div>
</div>
<% content_for(:page_title) do %> <% content_for(:page_title) do %>
Settings Settings
<% end %> <% end %>
<% content_for(:html_header) do %>
<%= javascript_tag nonce: true do -%>
$(function() {
$("#advanced-settings-section").hide();
$("#edit-options a:not(#delete-account):not(#change-password)").on("click", function(e) {
var $target = $(e.target);
$("h2 a").removeClass("active");
$("#basic-settings-section,#advanced-settings-section").hide();
$target.addClass("active");
$($target.attr("href") + "-section").show();
e.preventDefault();
});
});
<% end -%>
<% end %>
<%= render "secondary_links" %> <%= render "secondary_links" %>

View File

@ -0,0 +1,166 @@
<% tab_name = "advanced" %>
<!-- Accessibility -->
<tab-group name="accessibility">Accessibility</tab-group>
<tab-entry tab="<%= tab_name %>" group="accessibility" class="inline" search="keyboard hotkeys shortcut navigation enable disable">
<tab-head>Enable Keyboard Shortcuts</tab-head>
<tab-body>
<%= form.input_field :enable_keyboard_navigation, as: :boolean, class: "st-toggle" %>
<%= form.label :enable_keyboard_navigation, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
The full list of shortcuts is available <%= link_to "here", keyboard_shortcuts_path %>.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="accessibility" class="inline" search="auto complete autocomplete suggestions enable disable">
<tab-head>Enable Auto Complete</tab-head>
<tab-body>
<%= form.input_field :enable_auto_complete, as: :boolean, class: "st-toggle" %>
<%= form.label :enable_auto_complete, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Tag and user name suggestions.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="accessibility" class="inline" search="users colored usernames level rank">
<tab-head>Colored Usernames</tab-head>
<tab-body>
<%= form.input_field :style_usernames, as: :boolean, class: "st-toggle" %>
<%= form.label :style_usernames, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Color names depending on the user's level.
</tab-hint>
</tab-entry>
<!-- Privacy & Messaging -->
<tab-group name="privacy">Privacy & Messaging</tab-group>
<tab-entry tab="<%= tab_name %>" group="privacy" class="inline" search="posts favorites hide disable private privacy">
<tab-head>Hide Favorites</tab-head>
<tab-body>
<%= form.input_field :enable_privacy_mode, as: :boolean, class: "st-toggle" %>
<%= form.label :enable_privacy_mode, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Prevent your favorites from being publicly visible.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="privacy" class="inline" search="dmails filter banned block enable disable toggle">
<tab-head>Disable DMails</tab-head>
<tab-body>
<%= form.input_field :disable_user_dmails, as: :boolean, class: "st-toggle", disabled: CurrentUser.is_staff? %>
<%= form.label :disable_user_dmails, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Prevent other users from sending you DMails.
<% if CurrentUser.is_staff? %>
<br />Staff members are not allowed to disable DMails.
<% end %>
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="privacy" search="dmails filter banned block">
<tab-head>DMail Filters</tab-head>
<tab-body>
<%= hidden_field_tag "user[dmail_filter_attributes][id]", @user.dmail_filter.try(:id) %>
<%= text_field_tag "user[dmail_filter_attributes][words]", @user.dmail_filter.try(:words), id: "user_dmail_filter_attributes_words", class: "text optional", size: 40 %>
</tab-body>
<tab-hint>
A list of banned words (space delimited). Any dmail you receive with a banned word will automatically be deleted.
</tab-hint>
</tab-entry>
<!-- Posts -->
<tab-group name="tags">Posts & Tags</tab-group>
<tab-entry tab="<%= tab_name %>" group="tags" class="inline" search="show hide posts thumbnails statistics stats search">
<tab-head>Show Statistics</tab-head>
<tab-body>
<%= form.input_field :show_post_statistics, as: :boolean, class: "st-toggle" %>
<%= form.label :show_post_statistics, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Show post stats below posts on search pages.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="tags" class="inline" search="show hide collapse expand posts descriptions">
<tab-head>Collapse Descriptions</tab-head>
<tab-body>
<%= form.input_field :description_collapsed_initially, as: :boolean, class: "st-toggle" %>
<%= form.label :description_collapsed_initially, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Do not expand post descriptions on page load.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="tags" class="bigtext" search="posts tags frequent favorite related">
<tab-head>Frequent Tags</tab-head>
<tab-body>
<%= form.input_field :favorite_tags, label: false, autocomplete: "tag-query", rows: 5 %>
</tab-body>
<tab-hint>
A list of tags that you use often.<br />They will appear when using the list of Related Tags.
</tab-hint>
</tab-entry>
<% if @user.post_upload_count >= 10 %>
<tab-entry tab="<%= tab_name %>" group="tags" class="inline" search="show hide compact uploader posts">
<tab-head>Compact Uploader</tab-head>
<tab-body>
<%= form.input_field :enable_compact_uploader, as: :boolean, class: "st-toggle" %>
<%= form.label :enable_compact_uploader, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
A more compact and less guided post uploader.
</tab-hint>
</tab-entry>
<% end %>
<!-- Mobile -->
<tab-group name="mobile">Mobile / Tablet</tab-group>
<tab-entry tab="<%= tab_name %>" group="mobile" class="inline" search="enable disable responsive mode mobile tablet">
<tab-head>Disable Responsive Mode</tab-head>
<tab-body>
<%= form.input_field :disable_responsive_mode, as: :boolean, class: "st-toggle" %>
<%= form.label :disable_responsive_mode, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Disable alternative layout for mobile and tablet.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="mobile" class="inline" search="enable disable cropped thumbnails mobile tablet">
<tab-head>Disable Cropped Thumbnails</tab-head>
<tab-body>
<%= form.input_field :disable_cropped_thumbnails, as: :boolean, class: "st-toggle" %>
<%= form.label :disable_cropped_thumbnails, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
No effect on the desktop site.
</tab-hint>
</tab-entry>
<!-- Customization -->
<tab-group name="customization">Customization</tab-group>
<tab-entry tab="<%= tab_name %>" group="customization" class="bigtext" search="customize css styles">
<tab-head>Custom CSS</tab-head>
<tab-body>
<%= form.input_field :custom_style, label: false, rows: 8 %>
</tab-body>
<tab-hint>
Apply <a href="https://en.wikipedia.org/wiki/Cascading_Style_Sheets">CSS Styles</a> to the whole website.
</tab-hint>
</tab-entry>

View File

@ -0,0 +1,155 @@
<% tab_name = "basic" %>
<!-- Account -->
<tab-group name="account">Account</tab-group>
<tab-entry tab="<%= tab_name %>" group="account" class="buttony" search="update change user profile account name username">
<tab-head>Username</tab-head>
<tab-body>
<input type="text" value="<%= CurrentUser.user.pretty_name %>" disabled><%= link_to "Edit", new_user_name_change_request_path, class: "st-button" %>
</tab-body>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="account" class="buttony" search="update change user profile account email e-mail address">
<tab-head>Email</tab-head>
<tab-body>
<input type="text" value="<%= CurrentUser.user.email.presence || "blank" %>" disabled><%= link_to "Edit", new_maintenance_user_email_change_path, class: "st-button" %>
</tab-body>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="account" class="" id="settings-account-buttons" search="update change user profile account delete password">
<tab-head></tab-head>
<tab-body>
<%= link_to "Change password", edit_user_password_path(user_id: @user.id), class: "st-button" %>
<%= link_to "Delete account", maintenance_user_deletion_path, class: "st-button danger" %>
</tab-body>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="account" search="user account timezone">
<tab-head>Time Zone</tab-head>
<tab-body>
<%= form.input_field :time_zone,
label: false,
include_blank: false
%>
</tab-body>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="account" class="inline" search="user account email notify notifications">
<tab-head>Email Notifications</tab-head>
<tab-body>
<%= form.input_field :receive_email_notifications, as: :boolean, class: "st-toggle" %>
<%= form.label :receive_email_notifications, "!", class: "st-toggle" %>
</tab-body>
</tab-entry>
<!-- General -->
<!-- Profile -->
<tab-group name="profile">Profile</tab-group>
<tab-entry tab="<%= tab_name %>" group="profile" search="update change user avatar profile image pfp">
<tab-head><%= form.label :avatar_id, "Avatar Post ID" %></tab-head>
<tab-body>
<%= form.input_field :avatar_id, as: :string, label: false %>
</tab-body>
<tab-hint>
The image with this ID will be set as your avatar.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="profile" class="bigtext" search="user profile about me">
<tab-head><%= form.label :profile_about, "About Me" %></tab-head>
<tab-body>
<%= form.input_field :profile_about,
as: :dtext,
label: false,
rows: 8,
limit: Danbooru.config.user_about_max_size,
allow_color: true
%>
</tab-body>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="profile" class="bigtext" search="user profile commission info">
<tab-head><%= form.label :profile_artinfo, "Commission Info" %></tab-head>
<tab-body>
<%= form.input_field :profile_artinfo,
as: :dtext,
label: false,
rows: 8,
limit: Danbooru.config.user_about_max_size,
allow_color: true
%>
</tab-body>
</tab-entry>
<!-- Posts -->
<tab-group name="posts">Posts</tab-group>
<tab-entry tab="<%= tab_name %>" group="posts" search="default images width fit horizontal vertical sample large original display show scale posts">
<tab-head><%= form.label :default_image_size, "Default image width" %></tab-head>
<tab-body>
<%= form.input_field :default_image_size,
label: false,
collection: [["Original", "original"], ["Fit (Horizontal)", "fit"], ["Fit (Vertical)", "fitv"], ["Sample (#{Danbooru.config.large_image_width}px)", "large"]],
include_blank: false
%>
</tab-body>
<tab-hint>
Show original image size, scaled to fit, scaled to fit vertically, or show resized 850 pixel sample version.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="posts" search="posts number page display show">
<tab-head><%= form.label :per_page, "Posts per page" %></tab-head>
<tab-body>
<%= form.input_field :per_page, label: false, as: :select, collection: (25..250).step(25), include_blank: false %>
</tab-body>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="posts" search="comments score rating upvotes downvotes display show hide">
<tab-head><%= form.label :comment_threshold %></tab-head>
<tab-body>
<%= form.input_field :comment_threshold, label: false %>
</tab-body>
<tab-hint>
Comments below this score will be hidden by default.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="posts" class="inline" search="posts comments show hide display hidden">
<tab-head><%= form.label :hide_comments %></tab-head>
<tab-body>
<%= form.input_field :hide_comments, as: :boolean, class: "st-toggle" %>
<%= form.label :hide_comments, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Do not show the comments section on post pages.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="posts" class="inline" search="posts comments show hide display own hidden">
<tab-head><%= form.label :show_hidden_comments, "Show own hidden comments" %></tab-head>
<tab-body>
<%= form.input_field :show_hidden_comments, as: :boolean, class: "st-toggle" %>
<%= form.label :show_hidden_comments, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Show your hidden comments on comment pages.
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="posts" class="inline" search="posts display show hide mode explicit questionable safe rated rating">
<tab-head><%= form.label :enable_safe_mode, "Safe mode" %></tab-head>
<tab-body>
<%= form.input_field :enable_safe_mode, as: :boolean, class: "st-toggle" %>
<%= form.label :enable_safe_mode, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Only show images rated safe.
</tab-hint>
</tab-entry>

View File

@ -0,0 +1,25 @@
<% tab_name = "blacklist" %>
<!-- Accessibility -->
<tab-group name="blacklisting">Blacklist</tab-group>
<tab-entry tab="<%= tab_name %>" group="blacklisting" class="bigtext" search="posts blacklisted filtered tags blocked">
<tab-head>Blacklisted Tags</tab-head>
<tab-body>
<%= form.input_field :blacklisted_tags, label: false, autocomplete: "tag-query", rows: 8 %>
</tab-body>
<tab-hint>
Put any tag combinations you never want to see here. Each combination should go on a separate line. <a href='/help/blacklist'>View help.</a>
</tab-hint>
</tab-entry>
<tab-entry tab="<%= tab_name %>" group="blacklisting" class="inline" search="posts blacklisted filter users comments blips forum">
<tab-head>Blacklist Users</tab-head>
<tab-body>
<%= form.input_field :blacklist_users, as: :boolean, class: "st-toggle" %>
<%= form.label :blacklist_users, "!", class: "st-toggle" %>
</tab-body>
<tab-hint>
Hide comments, blips and forum posts from users that have been blacklisted, in addition to posts.
</tab-hint>
</tab-entry>

View File

@ -0,0 +1,17 @@
<% if has_about %>
<tab-entry tab="about" class="profile-about-section flex">
<tab-head>About</tab-head>
<tab-body class="content dtext-container">
<%= format_text(user.profile_about, allow_color: true) %>
</tab-body>
</tab-entry>
<% end %>
<% if has_artinfo %>
<tab-entry tab="artinfo" class="profile-artinfo-section flex">
<tab-head>Artist Information</tab-head>
<tab-body class="content dtext-container">
<%= format_text(user.profile_artinfo, allow_color: true) %>
</tab-body>
</tab-entry>
<% end %>

View File

@ -0,0 +1,5 @@
<% if user.is_banned? && user.recent_ban %>
<div class="profile-ban dtext-container">
<%= format_text presenter.ban_reason %>
</div>
<% end %>

View File

@ -0,0 +1,20 @@
<div class="profile-card">
<div class="profile-avatar">
<%= profile_avatar(@user) %>
</div>
<div class="profile-info">
<span class="profile-name">
<%= link_to_user(@user) %>
<%= user_feedback_badge(@user) %>
</span>
<span class="profile-joined">
Joined <%= compact_date @user.created_at %>
</span>
<span class="profile-rank">
<%= user_level_badge(@user) %>
<% unless @user.is_verified? %>
<span class="level-badge level-unactivated">UNACTIVATED</span>
<% end %>
</span>
</div>
</div>

View File

@ -4,7 +4,7 @@
<div class="profile-sample profile-uploads"> <div class="profile-sample profile-uploads">
<div class="profile-sample-header"> <div class="profile-sample-header">
<%= link_to "Uploads", posts_path(:tags => "user:#{user.name}"), class: "title" %> <%= link_to "Uploads", posts_path(tags: "user:#{user.name}"), class: "title" %>
</div> </div>
<div class="profile-sample-links"> <div class="profile-sample-links">
@ -29,11 +29,11 @@
<div class="profile-sample profile-favorites"> <div class="profile-sample profile-favorites">
<div class="profile-sample-header"> <div class="profile-sample-header">
<%= link_to "Favorites", favorites_path(:user_id => user.id), class: "title" %> <%= link_to "Favorites", favorites_path(user_id: user.id), class: "title" %>
</div> </div>
<div class="profile-sample-links"> <div class="profile-sample-links">
<%= link_to(sanitize("<b>#{user.favorite_count}</b> total"), favorites_path(:user_id => user.id)) %> <%= link_to(sanitize("<b>#{user.favorite_count}</b> total"), favorites_path(user_id: user.id)) %>
<% if user.enable_privacy_mode? || user.is_blocked? %> <% if user.enable_privacy_mode? || user.is_blocked? %>
<span>[hidden]</span> <span>[hidden]</span>
<% end %> <% end %>

View File

@ -0,0 +1,34 @@
<div class="profile-section hidden" name="StaffStats">
<div class="profile-section-header">Staff Info</div>
<div class="profile-section-body profile-staff-info">
<% if presenter.previous_names(self).present? %>
<h4 class="block">Previous Names</h4>
<span class="block"><%= presenter.previous_names(self) %> -> <%= user.name %></span>
<% end %>
<% if CurrentUser.is_admin? %>
<h4>Email</h4>
<span>
<%= user.email %>
<%= email_domain_search(user.email) %>
</span>
<h4>Last IP</h4>
<span><%= link_to_ip(user.last_ip_addr) %></span>
<% end %>
<% if CurrentUser.is_moderator? %>
<h4>Votes</h4>
<span>
<%= link_to "Posts", action: "index", controller: "post_votes", search: { user_name: user.name } %>
| <%= link_to "Comments", action: "index", controller: "comment_votes", search: { user_name: user.name } %>
</span>
<% end %>
<h4>Pending</h4>
<span>
<%= link_to "Posts", posts_path(tags: "user:#{user.name} status:pending") %>
| <%= link_to "Replacements", post_replacements_path(search: { creator_name: user.name }) %>
</span>
</div>
</div>

View File

@ -0,0 +1,129 @@
<tab-entry tab="stats" class="profile-user-info">
<div class="profile-line">
<h4><%= svg_icon(:message_square) %> Comments</h4>
<span class="profile-line-number">
<%= presenter.comment_count(self) %>
</span>
<span class="profile-line-extra">
<%= link_to "[mentions]", comments_path(group_by: :comment, search: { body_matches: user.name }) %>
</span>
</div>
<div class="profile-line">
<h4><%= svg_icon(:lectern) %> Forum posts</h4>
<span class="profile-line-number">
<%= presenter.forum_post_count(self) %>
</span>
<span class="profile-line-extra">
<%= link_to "[mentions]", forum_posts_path(search: { body_matches: user.name }) %>
</span>
</div>
<% if user.can_approve_posts? || Post.where(approver: user).exists? %>
<div class="profile-line">
<h4><%= svg_icon(:stamp) %> Approvals</h4>
<span class="profile-line-number">
<%= presenter.approval_count(self) %>
</span>
</div>
<% end %>
<div class="profile-line">
<h4><%= svg_icon(:upload) %> Upload Limit</h4>
<span class="profile-line-number">
<% if CurrentUser.user.id == user.id %>
<%= link_to presenter.upload_limit_short, upload_limit_users_path %>
<% else %>
<%= link_to presenter.upload_limit_short, wiki_page_path(id: "upload_limit") %>
<% end %>
</span>
<span class="profile-line-extra">
Max number of unapproved posts at a time.
</span>
</div>
<div class="profile-line">
<h4><%= svg_icon(:replace) %> Changes</h4>
<span class="profile-line-extra profile-line-show">
<% if user.post_update_count != 0 %>
<a href="<%= post_versions_path(lr: user.id, search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.post_update_count %></h5>
<span>Post</span>
</a>
<% end %>
<% if user.pool_version_count != 0 %>
<a href="<%= pool_versions_path(search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.pool_version_count %></h5>
<span>Pool</span>
</a>
<% end %>
<% if user.artist_version_count != 0 %>
<a href="<%= artist_versions_path(search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.artist_version_count %></h5>
<span>Artist</span>
</a>
<% end %>
<% if user.wiki_page_version_count != 0 %>
<a href="<%= wiki_page_versions_path(search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.wiki_page_version_count %></h5>
<span>Wiki</span>
</a>
<% end %>
<% if user.note_version_count != 0 %>
<a href="<%= note_versions_path(search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.note_version_count %></h5>
<span>Note</span>
</a>
<% end %>
</span>
</div>
<% if CurrentUser.user.id == user.id || CurrentUser.is_approver? %>
<div class="profile-line">
<h4><%= svg_icon(:flag_left) %> Flags</h4>
<span class="profile-line-number"><%= presenter.flag_count(self) %></span>
</div>
<% end %>
<% if CurrentUser.user.id == user.id || CurrentUser.is_moderator? %>
<div class="profile-line">
<h4><%= svg_icon(:ticket) %> Tickets</h4>
<span class="profile-line-number"><%= presenter.ticket_count(self) %></span>
<% if CurrentUser.is_moderator? %>
<span class="profile-line-extra">
[<%= link_to "pending", tickets_path(search: { creator_id: user.id, status: "pending" }) %>]
[<%= link_to "accused", tickets_path(search: { accused_id: user.id }) %>]
</span>
<% end %>
</div>
<% end %>
<% if CurrentUser.user.id == user.id %>
<div class="profile-line">
<h4><%= svg_icon(:key_square) %> API Key</h4>
<span class="profile-line-number">
<%= link_to (CurrentUser.api_key ? "View" : "Generate"), user_api_key_path(CurrentUser.user) %>
</span>
<span class="profile-line-extra">
(<%= link_to "help", help_page_path(id: "api") %>)
</span>
</div>
<% end %>
<% permissions = presenter.permissions %>
<% unless permissions.empty? %>
<div class="profile-line">
<h4><%= svg_icon(:power) %> Permissions</h4>
<span class="profile-line-extra">
<%= permissions %>
</span>
</div>
<% end %>
</tab-entry>

View File

@ -1,10 +1,41 @@
<div id="c-users"> <div id="c-users">
<div id="a-show"> <div id="a-show">
<%= render "statistics", :presenter => @presenter, :user => @user %>
<%= render "staff_notes/partials/for_user", user: @user %> <!-- Header -->
<%= render "/users/partials/show/card", :presenter => @presenter, :user => @user %>
<%= render "/users/partials/show/ban_banner", :presenter => @presenter, :user => @user %>
<% if CurrentUser.is_staff? %>
<!-- Staff Info -->
<%= render "/users/partials/show/staff_info", :presenter => @presenter, :user => @user %>
<%= render "staff_notes/partials/for_user", user: @user %>
<% end %>
<!-- Blacklist -->
<%= render "posts/partials/common/inline_blacklist" %> <%= render "posts/partials/common/inline_blacklist" %>
<%= render "post_summary", presenter: @presenter, user: @user %>
<%= render "about", presenter: @presenter, user: @user %> <!-- Central panel -->
<% has_about = @user.profile_about.present? %>
<% has_artinfo = @user.profile_artinfo.present? %>
<tabs-menu
id="profile-tabs"
data-has-about="<%= has_about %>"
data-has-artinfo="<%= has_artinfo %>"
>
<% if has_about %>
<button role="tab" name="about">About</button>
<% end %>
<% if has_artinfo %>
<button role="tab" name="artinfo">Commission Info</button>
<% end %>
<button role="tab" name="stats">Stats</button>
</tabs-menu>
<tabs-content for="profile-tabs">
<%= render "/users/partials/show/user_info", :presenter => @presenter, :user => @user %>
<%= render "/users/partials/show/about", presenter: @presenter, user: @user, has_about: has_about, has_artinfo: has_artinfo %>
<%= render "/users/partials/show/post_summary", presenter: @presenter, user: @user %>
</tabs-content>
</div> </div>
</div> </div>

View File

@ -2,23 +2,19 @@
<div id="a-diff"> <div id="a-diff">
<h1>Wiki Page: <%= @thispage.title %></h1> <h1>Wiki Page: <%= @thispage.title %></h1>
<% if @thispage.visible? %> <p>Showing differences between <%= compact_time @thispage.updated_at %> (<%= link_to_user @thispage.updater %>) and <%= compact_time @otherpage.updated_at %> (<%= link_to_user @otherpage.updater %>)</p>
<p>Showing differences between <%= compact_time @thispage.updated_at %> (<%= link_to_user @thispage.updater %>) and <%= compact_time @otherpage.updated_at %> (<%= link_to_user @otherpage.updater %>)</p>
<% if @thispage.parent != @otherpage.parent %> <% if @thispage.parent != @otherpage.parent %>
<div class="wiki-redirect-history"> <div class="wiki-redirect-history">
Page redirect changed Page redirect changed
from <%= @thispage.parent.blank? ? "none" : link_to(@thispage.parent, show_or_new_wiki_pages_path(title: @thispage.parent)) %> from <%= @thispage.parent.blank? ? "none" : link_to(@thispage.parent, show_or_new_wiki_pages_path(title: @thispage.parent)) %>
to <%= @otherpage.parent.blank? ? "none" : link_to(@otherpage.parent, show_or_new_wiki_pages_path(title: @otherpage.parent)) %>. to <%= @otherpage.parent.blank? ? "none" : link_to(@otherpage.parent, show_or_new_wiki_pages_path(title: @otherpage.parent)) %>.
</div>
<% end %>
<div>
<%= text_diff(@thispage.body, @otherpage.body) %>
</div> </div>
<% else %>
<p>The artist requested removal of this page.</p>
<% end %> <% end %>
<div>
<%= text_diff(@thispage.body, @otherpage.body) %>
</div>
</div> </div>
</div> </div>

View File

@ -10,11 +10,7 @@
<% end %> <% end %>
<div id="wiki-page-body" class="dtext dtext-container"> <div id="wiki-page-body" class="dtext dtext-container">
<% if @wiki_page_version.visible? %> <%= format_text(@wiki_page_version.body) %>
<%= format_text(@wiki_page_version.body) %>
<% else %>
<p>The artist has requested removal of this page.</p>
<% end %>
</div> </div>
</section> </section>
</div> </div>

View File

@ -12,22 +12,28 @@
<%= f.input :body, as: :dtext, limit: Danbooru.config.wiki_page_max_size, allow_color: true %> <%= f.input :body, as: :dtext, limit: Danbooru.config.wiki_page_max_size, allow_color: true %>
<%= f.input :category_id,
label: "Tag Category",
collection: TagCategory::CANONICAL_MAPPING.to_a,
include_blank: true,
disabled: @wiki_page.category_is_locked && !CurrentUser.is_admin? %>
<% if CurrentUser.is_admin? %>
<%= f.input :category_is_locked, label: "Lock Category", as: :boolean %>
<% end %>
<%= f.input :parent, label: "Redirects to", autocomplete: "wiki-page", input_html: { disabled: !CurrentUser.is_privileged? } %> <%= f.input :parent, label: "Redirects to", autocomplete: "wiki-page", input_html: { disabled: !CurrentUser.is_privileged? } %>
<% if CurrentUser.is_janitor? && @wiki_page.is_deleted? %>
<%= f.input :is_deleted, :label => "Deleted", :hint => "Uncheck to restore this wiki page" %>
<% end %>
<% if CurrentUser.is_janitor? %> <% if CurrentUser.is_janitor? %>
<%= f.input :is_locked, :label => "Locked" %> <%= f.input :is_locked, :label => "Lock Page" %>
<% end %> <% end %>
<%= f.input :edit_reason, label: "Edit Reason" %>
<% if CurrentUser.is_janitor? && @wiki_page.errors[:title]&.any? { |error| error.include?("Move the posts and update any wikis linking to this page first.") } %> <% if CurrentUser.is_janitor? && @wiki_page.errors[:title]&.any? { |error| error.include?("Move the posts and update any wikis linking to this page first.") } %>
<%= f.input :skip_secondary_validations, as: :boolean, label: "Force rename", hint: "Ignore the renaming requirements" %> <%= f.input :skip_secondary_validations, as: :boolean, label: "Force rename", hint: "Ignore the renaming requirements" %>
<% end %> <% end %>
<%= f.input :edit_reason, label: "Edit Reason" %>
<%= f.button :submit, "Submit" %> <%= f.button :submit, "Submit" %>
<% end %> <% end %>
</div> </div>

View File

@ -17,11 +17,8 @@
<% if @wiki_page.tag.present? %> <% if @wiki_page.tag.present? %>
<%= subnav_link_to "Posts (#{@wiki_page.tag.post_count})", posts_path(tags: @wiki_page.title) %> <%= subnav_link_to "Posts (#{@wiki_page.tag.post_count})", posts_path(tags: @wiki_page.title) %>
<% if CurrentUser.is_member? %> <% if CurrentUser.is_janitor?%>
<%= subnav_link_to "Edit Tag Type", edit_tag_path(@wiki_page.tag) %> <%= subnav_link_to "Fix Tag Count", new_tag_correction_path(tag_id: @wiki_page.tag.id) %>
<% if CurrentUser.is_janitor? %>
<%= subnav_link_to "Fix Tag Count", new_tag_correction_path(tag_id: @wiki_page.tag.id) %>
<% end %>
<% end %> <% end %>
<% end %> <% end %>
@ -40,6 +37,8 @@
<%= subnav_link_to "Report", new_ticket_path(disp_id: @wiki_page.id, qtype: "wiki") %> <%= subnav_link_to "Report", new_ticket_path(disp_id: @wiki_page.id, qtype: "wiki") %>
<% end %> <% end %>
<% end %> <% end %>
<% elsif @wiki_page_version %> <% elsif @wiki_page_version %>
<li class="divider"></li> <li class="divider"></li>

View File

@ -5,11 +5,7 @@
<section id="content"> <section id="content">
<h1>Edit Wiki</h1> <h1>Edit Wiki</h1>
<% if @wiki_page.visible? %> <%= render "form" %>
<%= render "form" %>
<% else %>
<p>The artist requested removal of this page.</p>
<% end %>
<%= wiki_page_alias_and_implication_list(@wiki_page)%> <%= wiki_page_alias_and_implication_list(@wiki_page)%>
<%= wiki_page_post_previews(@wiki_page) %> <%= wiki_page_post_previews(@wiki_page) %>

View File

@ -21,17 +21,13 @@
<% end %> <% end %>
<div id="wiki-page-body" class="dtext-container"> <div id="wiki-page-body" class="dtext-container">
<% if wiki_content.visible? %> <%= format_text(wiki_content.body, allow_color: true, max_thumbs: 75) %>
<%= format_text(wiki_content.body, allow_color: true, max_thumbs: 75) %>
<% if wiki_content.artist %> <% if wiki_content.artist %>
<p><%= link_to "View artist", wiki_content.artist %></p> <p><%= link_to "View artist", wiki_content.artist %></p>
<% end %>
<%= wiki_page_alias_and_implication_list(wiki_content) %>
<% else %>
<p>This artist has requested removal of their information.</p>
<% end %> <% end %>
<%= wiki_page_alias_and_implication_list(wiki_content) %>
</div> </div>
<%= wiki_page_post_previews(wiki_content) %> <%= wiki_page_post_previews(wiki_content) %>

View File

@ -8,23 +8,26 @@ destroyed_feedback_ids = []
CurrentUser.as_system do CurrentUser.as_system do
ModAction.where(action: "user_feedback_destroy") ModAction.where(action: "user_feedback_destroy")
# On July 24, 2024, we deployed the ability to soft-delete feedback records. # On July 24, 2024, we deployed the ability to soft-delete feedback records.
# We only care about restoring destroyed feedbacks that were destroyed before this date. # We only restore feedback that was destroyed before this date.
# Any entries after this are "real" destructions, that do not need to be restored. # Any entries after this are intentional destructions that do not need to be restored.
.where("created_at < ?", CUTOFF_DATE = Date.new(2024, 8, 1)) .where("created_at < ?", CUTOFF_DATE = Date.new(2024, 8, 1))
.find_in_batches(batch_size: 10_000) do |batch| .find_in_batches(batch_size: 10_000) do |batch|
feedback_data = batch.map do |mod_action| feedback_data = batch.filter_map do |mod_action|
record_id = mod_action.values["record_id"].to_i record_id = mod_action.values["record_id"]
category = mod_action.values["type"]
body = mod_action.values["reason"]
# Old mod actions do not contain the necessary information. Skip them.
next if record_id.nil? || category.nil? || body.nil?
destroyed_feedback_ids << record_id destroyed_feedback_ids << record_id
# old mod actions do not contain the necessary information. we skip them.
next if mod_action.values["type"].nil? || mod_action.values["reason"].nil?
{ {
id: record_id, id: record_id.to_i,
user_id: mod_action.values["user_id"].to_i, user_id: mod_action.values["user_id"].to_i,
creator_id: User.system.id, # placeholder creator_id: User.system.id, # placeholder
category: mod_action.values["type"], category: category,
body: mod_action.values["reason"]&.strip, body: body.strip,
created_at: Date.new(1970, 1, 1), # placeholder created_at: Date.new(1970, 1, 1), # placeholder
updated_at: mod_action.created_at, updated_at: mod_action.created_at,
updater_id: mod_action.creator_id, updater_id: mod_action.creator_id,

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More