This commit is contained in:
Eva
2025-03-30 06:05:54 +00:00
committed by GitHub
36 changed files with 141 additions and 44 deletions

View File

@ -16,7 +16,13 @@ $comment-border-color = #DDD
width: 40px
height: 40px
a
outline: none
display: inline-block
position: relative
top: -2px
border: 2px solid transparent
&:focus
border: 2px solid $main-color
nav:not(.active), .tab:not(.active)
display: none

View File

@ -125,6 +125,9 @@ input[type=radio]:checked + .radio:after,
input[type=checkbox]:checked + .checkbox:after
opacity: 1
input[type=radio]:disabled + .radio:after
background: $input-disabled-text-color
input[type=radio]:disabled + .radio:before,
input[type=checkbox]:disabled + .checkbox:before,
input[type=radio]:disabled + .radio:after,
@ -203,6 +206,7 @@ input[type=number]
background: $input-enabled-background-color
color: $input-enabled-text-color
box-shadow: none /* :-moz-submit-invalid on FF */
outline: none
transition: border-color 0.1s linear, background-color 0.1s linear
&:disabled
@ -211,7 +215,7 @@ input[type=number]
color: $input-disabled-text-color
&:focus
border-color: $main-color
border-color: $main-color !important
&[readonly]
border: 2px solid $input-disabled-border-color

View File

@ -106,7 +106,12 @@ form .fa-question-circle-o
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
>.content-wrapper:not(.transparent)
li[data-name=view]
background: $button-enabled-background-color
margin-right: 1em
a
color: $button-enabled-text-color
>.content-wrapper:not(.transparent-container)
background: $top-navigation-color
padding: 1.8em
@media (max-width: 1000px)
@ -117,7 +122,7 @@ form .fa-question-circle-o
margin-bottom: 0
.darktheme #content-holder
>.content-wrapper:not(.transparent)
>.content-wrapper:not(.transparent-container)
background: $top-navigation-color-darktheme
hr

View File

@ -61,7 +61,10 @@
word-spacing: 1.1em
background-repeat: no-repeat
background-position: 50% 50%
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><circle cx='6' cy='6' r='2' fill='%23000000'/></svg>")
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><circle cx='6' cy='6' r='2' fill='%23111111'/></svg>")
.thumbnail
margin-right: 0.4em
.darktheme #home footer ul .sep
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><circle cx='6' cy='6' r='2' fill='%23e6e6e6'/></svg>")

View File

@ -18,6 +18,8 @@
right: 0
top: 0
bottom: 0
&[data-state=read-only]
pointer-events: none
.notes-overlay
g

View File

@ -145,6 +145,7 @@ $cancel-button-color = tomato
clear: both
margin: 1em 0 0 0
padding-left: 7em
padding-bottom: 1em;
font-size: 90%
.thumbnail-wrapper

View File

@ -34,6 +34,16 @@ shortcuts:</p>
<td>Focus first post in post list</td>
</tr>
<tr>
<td><kbd>T</kbd></td>
<td>(In edit mode) Focus tag input</td>
</tr>
<tr>
<td><kbd>Command/Ctrl+S</kbd></td>
<td>(In edit mode) Save post</td>
</tr>
<tr>
<td><kbd>Delete</kbd></td>
<td>Delete post (while in edit mode)</td>

View File

@ -1,4 +1,4 @@
<div class='content-wrapper transparent' id='home'>
<div class='content-wrapper transparent-container' id='home'>
<div class='messages'></div>
<header>
<h1><%- ctx.name %></h1>

View File

@ -2,6 +2,7 @@
<h1><%- ctx.getPrettyName(ctx.pool.names[0]) %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='view'><a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'>View</a></li><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'edit') %>'>Edit</a></li><!--

View File

@ -1,4 +1,10 @@
<div class='content-wrapper pool-summary'>
<section class='description'>
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
<hr/>
</section>
<section class='details'>
<section>
Category:
@ -14,10 +20,4 @@
--></ul>
</section>
</section>
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
</section>
</div>

View File

@ -1,7 +1,7 @@
<div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(ctx.post.type)) { %>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>' draggable='false'/>
<% } else if (ctx.post.type === 'flash') { %>

View File

@ -1,4 +1,4 @@
<div class='content-wrapper transparent post-view'>
<div class='content-wrapper transparent-container post-view'>
<aside class='sidebar'>
<nav class='buttons'>
<article class='previous-post'>

View File

@ -1,7 +1,7 @@
<div class='readonly-sidebar'>
<article class='details'>
<section class='download'>
<a rel='external' href='<%- ctx.post.contentUrl %>'>
<a href='<%- ctx.post.contentUrl %>' download>
<i class='fa fa-download'></i><!--
--><%= ctx.makeFileSize(ctx.post.fileSize) %> <!--
--><%- {

View File

@ -85,7 +85,7 @@
<div class='controls'>
<%= ctx.makeCheckbox({text: 'Copy tags', name: 'copy-tags'}) %>
<br/>
<%= ctx.makeCheckbox({text: 'Add relation', name: 'add-relation'}) %>
<%= ctx.makeCheckbox({text: 'Add relation', name: 'add-relation', checked: true}) %>
</div>
</li>
<% } %>

View File

@ -10,28 +10,27 @@
<span class='type' data-type='<%- post.type %>'>
<% if (post.type == 'video' || post.type == 'flash' || post.type == 'animation') { %>
<span class='icon'><i class='fa fa-film'></i></span>
<% } else { %>
<%- post.type %>
<% } %>
<%- post.type %>
</span>
<% if (post.score || post.favoriteCount || post.commentCount) { %>
<span class='stats'>
<% if (post.score) { %>
<span class='icon'>
<i class='fa fa-thumbs-up'></i>
<%- post.score %>
<span style="display: none">score:</span><%- post.score %>
</span>
<% } %>
<% if (post.favoriteCount) { %>
<span class='icon'>
<i class='fa fa-heart'></i>
<%- post.favoriteCount %>
<span style="display: none">favorites:</span><%- post.favoriteCount %>
</span>
<% } %>
<% if (post.commentCount) { %>
<span class='icon'>
<i class='fa fa-commenting'></i>
<%- post.commentCount %>
<span style="display: none">comments:</span><%- post.commentCount %>
</span>
<% } %>
</span>

View File

@ -28,7 +28,6 @@
name: 'dark-theme',
checked: ctx.browsingSettings.darkTheme,
}) %>
<p class='hint'>Changing this setting will require you to refresh the page for it to apply.</p>
</li>
<li>

View File

@ -12,7 +12,7 @@
<%= item.operation %>
<%= ctx.makeResourceLink(item.type, item.id) %>
<%= ctx.makeResourceLink(item.type, item.id, item.data) %>
</div>
<div class='details'><!--

View File

@ -2,6 +2,7 @@
<h1><%- ctx.getPrettyName(ctx.tag.names[0]) %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='view'><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'>View</a></li><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0]) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'edit') %>'>Edit</a></li><!--

View File

@ -1,4 +1,10 @@
<div class='content-wrapper tag-summary'>
<section class='description'>
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<hr/>
</section>
<section class='details'>
<section>
Category:
@ -32,10 +38,4 @@
--></ul>
</section>
</section>
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
</section>
</div>

View File

@ -398,7 +398,7 @@ class Api extends events.EventTarget {
if (data) {
if (files && Object.keys(files).length) {
req.attach("metadata", new Blob([JSON.stringify(data)]));
req.attach("metadata", new Blob([JSON.stringify(data)], { type: "application/json" }));
} else {
req.set("Content-Type", "application/json");
req.send(data);

View File

@ -281,7 +281,16 @@ module.exports = (router) => {
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, true);
const canEditPosts = api.hasPrivilege("posts:edit");
if (canEditPosts) {
ctx.controller = new PostMainController(ctx, true);
} else {
router.show(uri.formatClientLink(
"post",
ctx.parameters.id,
ctx.parameters ? { query: ctx.parameters.query } : {}
));
}
});
router.enter(["post", ":id"], (ctx, next) => {
// restore parameters from history state

View File

@ -4,6 +4,7 @@ const api = require("../api.js");
const events = require("../events.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const keyboard = require("../util/keyboard.js");
const Note = require("../models/note.js");
const Point = require("../models/point.js");
const TagInputControl = require("./tag_input_control.js");
@ -224,10 +225,12 @@ class PostEditSidebarControl extends events.EventTarget {
});
}
this._tagControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
this._syncExpanderTitles();
});
if (this._tagControl) {
this._tagControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
this._syncExpanderTitles();
});
}
if (this._noteTextareaNode) {
this._noteTextareaNode.addEventListener("change", (e) =>
@ -241,6 +244,16 @@ class PostEditSidebarControl extends events.EventTarget {
this._syncExpanderTitles();
});
}
keyboard.bind(["command+s", "ctrl+s"], (e) => this._evtSubmit(e));
if (this._tagInputNode) {
const realTagInput = this._formNode.querySelector(".tag-input input");
keyboard.bindElement(realTagInput, ["command+s", "ctrl+s"], (e) => this._evtSubmit(e));
keyboard.bind("t", (e) => {
e.preventDefault();
realTagInput.focus();
});
}
}
_syncExpanderTitles() {

View File

@ -42,6 +42,10 @@ const pools = require("./pools.js");
const api = require("./api.js");
const settings = require("./models/settings.js");
if (settings.get().darkTheme) {
document.body.classList.add("darktheme");
}
Promise.resolve()
.then(() => api.fetchConfig())
.then(

View File

@ -75,7 +75,7 @@ class Post extends events.EventTarget {
}
get sourceSplit() {
return this._source.split("\n");
return this._source.split("\n").filter((s) => s);
}
get canvasWidth() {

View File

@ -40,6 +40,11 @@ class Settings extends events.EventTarget {
save(newSettings, silent) {
newSettings = Object.assign(this.cache, newSettings);
localStorage.setItem("settings", JSON.stringify(newSettings));
if (newSettings.darkTheme) {
document.body.classList.add("darktheme");
} else {
document.body.classList.remove("darktheme");
}
this.cache = this._getFromLocalStorage();
if (silent !== true) {
this.dispatchEvent(

View File

@ -22,12 +22,21 @@ function bind(hotkey, func) {
return false;
}
function bindElement(element, hotkey, func) {
if (settings.get().keyboardShortcuts) {
mousetrap(element).bind(hotkey, func);
return true;
}
return false;
}
function unbind(hotkey) {
mousetrap.unbind(hotkey);
}
module.exports = {
bind: bind,
bindElement: bindElement,
unbind: unbind,
pause: () => {
paused = true;

View File

@ -54,7 +54,7 @@ class TildeWrapper extends BaseMarkdownWrapper {
// prevent ^#... from being treated as headers, due to tag permalinks
class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
preprocess(text) {
return text.replace(/^#/g, "%%%#");
return text.replace(/^#(?=[a-zA-Z0-9_-])/g, "%%%#");
}
postprocess(text) {

View File

@ -5,7 +5,8 @@ const keyboard = require("../util/keyboard.js");
const views = require("./views.js");
function searchInputNodeFocusHelper(inputNode) {
keyboard.bind("q", () => {
keyboard.bind("q", (e) => {
e.preventDefault();
inputNode.focus();
inputNode.setSelectionRange(
inputNode.value.length,

View File

@ -111,7 +111,7 @@ function makeSelect(options) {
}
function makeInput(options) {
options.value = options.value || "";
options.value = options.value === 0 ? 0 : options.value || "";
return _makeLabel(options) + makeElement("input", options);
}
@ -261,7 +261,7 @@ function makePoolLink(id, includeHash, includeCount, pool, name) {
}
function makeUserLink(user) {
let text = makeThumbnail(user ? user.avatarUrl : null);
let text = makeThumbnail(user ? user.avatarUrl : "img/favicon.png");
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
const link =
user && user.name && api.hasPrivilege("users:view")
@ -300,6 +300,8 @@ function _serializeElement(name, attributes) {
attributes[key] === undefined
) {
return "";
} else if (attributes[key] === 0) {
return `${key}="0"`;
}
const attribute = misc.escapeHtml(attributes[key] || "");
return `${key}="${attribute}"`;
@ -321,6 +323,7 @@ function emptyContent(target) {
}
function replaceContent(target, source) {
if (!target) return;
emptyContent(target);
if (source instanceof NodeList) {
for (let child of [...source]) {

View File

@ -58,6 +58,11 @@ class PoolMergeView extends events.EventTarget {
_evtSubmit(e) {
e.preventDefault();
if (!this._targetPoolId) {
this.clearMessages();
this.showError("You must select a pool name from autocomplete.");
return;
}
this.dispatchEvent(
new CustomEvent("submit", {
detail: {

View File

@ -60,12 +60,14 @@ class BulkSafetyEditor extends BulkEditor {
e.preventDefault();
this.toggleOpen(true);
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
misc.enableExitConfirmation();
}
_evtCloseLinkClick(e) {
e.preventDefault();
this.toggleOpen(false);
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
misc.disableExitConfirmation();
}
}
@ -130,6 +132,7 @@ class BulkTagEditor extends BulkEditor {
this.toggleOpen(true);
this.focus();
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
misc.enableExitConfirmation();
}
_evtCloseLinkClick(e) {
@ -138,6 +141,7 @@ class BulkTagEditor extends BulkEditor {
this.toggleOpen(false);
this.blur();
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
misc.disableExitConfirmation();
}
}
@ -160,12 +164,14 @@ class BulkDeleteEditor extends BulkEditor {
e.preventDefault();
this.toggleOpen(true);
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
misc.enableExitConfirmation();
}
_evtCloseLinkClick(e) {
e.preventDefault();
this.toggleOpen(false);
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
misc.disableExitConfirmation();
}
}

View File

@ -29,7 +29,7 @@ function _formatBasicChange(diff, text) {
return lines;
}
function _makeResourceLink(type, id) {
function _makeResourceLink(type, id, data) {
if (type === "post") {
return views.makePostLink(id, true);
} else if (type === "tag") {
@ -37,7 +37,7 @@ function _makeResourceLink(type, id) {
} else if (type === "tag_category") {
return 'category "' + id + '"';
} else if (type === "pool") {
return views.makePoolLink(id, true);
return views.makePoolLink(id, false, false, data);
}
}

View File

@ -21,7 +21,7 @@ _search_executor = search.Executor(_search_executor_config)
def _get_post_id(params: Dict[str, str]) -> int:
try:
return int(params["post_id"])
except TypeError:
except (TypeError, ValueError):
raise posts.InvalidPostIdError(
"Invalid post ID: %r." % params["post_id"]
)

View File

@ -24,6 +24,11 @@ def convert_heif_to_png(content: bytes) -> bytes:
return img_byte_arr.getvalue()
def check_for_loop(content: bytes) -> bytes:
img = PILImage.open(BytesIO(content))
return "loop" in img.info
class Image:
def __init__(self, content: bytes) -> None:
self.content = content

View File

@ -531,8 +531,10 @@ def generate_alternate_formats(
def get_default_flags(content: bytes) -> List[str]:
assert content
ret = []
if mime.is_video(mime.get_mime_type(content)):
ret.append(model.Post.FLAG_LOOP)
if mime.is_animated_gif(content):
if images.check_for_loop(content):
ret.append(model.Post.FLAG_LOOP)
elif mime.is_video(mime.get_mime_type(content)):
if images.Image(content).check_for_sound():
ret.append(model.Post.FLAG_SOUND)
return ret

View File

@ -32,9 +32,13 @@ def _type_transformer(value: str) -> str:
def _safety_transformer(value: str) -> str:
available_values = {
"safe": model.Post.SAFETY_SAFE,
"s": model.Post.SAFETY_SAFE,
"sketchy": model.Post.SAFETY_SKETCHY,
"questionable": model.Post.SAFETY_SKETCHY,
"q": model.Post.SAFETY_SKETCHY,
"unsafe": model.Post.SAFETY_UNSAFE,
"explicit": model.Post.SAFETY_UNSAFE,
"e": model.Post.SAFETY_UNSAFE,
}
return search_util.enum_transformer(available_values, value)