41 Commits

Author SHA1 Message Date
Eva
441bad03e2 Merge 2e1534f2bb into 376f687c38 2025-04-03 01:17:40 +00:00
Eva
2e1534f2bb client/css: remove semicolons 2025-04-03 03:17:33 +02:00
Eva
a37ca56fa9 server: add tagme flag on upload, clear on edit
To easily filter through posts that haven't been retagged since initial
upload.
2025-04-02 21:47:09 +02:00
Eva
70d29da933 client/api: do a single info api call
Initial calls to api.fetchConfig() in quick succession would launch
multiple requests.
This should be implemented in Info tbh not outside of it.
2025-04-01 10:12:35 +02:00
Eva
362a86712a server/posts: optimized query for fetching post relations
This was taking multiple seconds, now it's <200ms for the whole
/post/ api request. Updating relations is still slow, but that doesn't
impact user experience.
2025-04-01 09:35:46 +02:00
Eva
f12e28855b client: update stylus for :has support 2025-04-01 07:30:55 +02:00
e21cd61919 client/tags: quicker access to the tagged posts
Based on #656, plus escaping.
2025-04-01 05:14:18 +02:00
Eva
223bc9674a server/posts: early return in own favorite serialization 2025-04-01 04:18:21 +02:00
Eva
04f4558a3b server/favorites: fix typing
user can never be None, we do an assert here and everywhere else this
function is called.
2025-04-01 03:42:40 +02:00
Eva
9f533882bf client/css: focused active tab styling 2025-04-01 01:48:59 +02:00
Eva
6035278c7e client/css: dark theme comment styling 2025-04-01 01:00:12 +02:00
Eva
acb1eddb53 client/css: text input focus outline 2025-03-30 08:05:47 +02:00
Eva
b3fa26b3fb client/api: set json mime type for metadata 2025-03-29 07:16:08 +01:00
Eva
d3c2da4f91 server/upload: disable default loop flag for videos, detect gif loop 2025-03-29 07:13:56 +01:00
Eva
8acd6ab776 server/search: add safety search synonyms 2025-03-29 07:13:24 +01:00
Eva
2ed19e013a server/posts: more thorough error exception 2025-03-29 07:13:10 +01:00
2b06e1cafa client: fix weird Chrome bug where video controls are invisible under div class="transparent" 2025-03-29 07:12:01 +01:00
7f6211e0cc client: fix displaying 0 in number inputs 2025-03-29 07:11:28 +01:00
Eva
86bbd429f7 client: apply theme as soon as possible
Fixes the default theme flashing on reload
2025-03-29 07:10:55 +01:00
Eva
0b02826e6d client: apply dark theme without a page reload
I'm really confused why the user was expected to reload the page,
there's no technical reason that I can find for it to be this way.
2025-03-29 07:08:33 +01:00
ea215901af client: add more keybinds, prevent typing when focusing input 2025-03-29 07:08:08 +01:00
Eva
9884161297 client/views: prevent errors on slow connections 2025-03-29 07:07:42 +01:00
Eva
b2501b7ee2 client/users: default user icon to site favicon for anonymous uploads 2025-03-29 07:07:09 +01:00
Eva
7f2a8b5b07 client/upload: check "Add relation" box by default 2025-03-29 07:06:49 +01:00
Eva
de9b8fdce3 client/snapshots: display pool name 2025-03-29 07:05:55 +01:00
Eva
ae85605531 client/posts: filter out empty post source lines 2025-03-29 07:05:34 +01:00
Eva
ae72d75631 client/posts: redirect away from /edit when no post edit permissions 2025-03-29 07:03:48 +01:00
Eva
ce613c5ade client/posts: page exit confirmation for bulk edit actions 2025-03-29 07:03:30 +01:00
Eva
e05252f67e client/posts: hide edit icon when unprivileged 2025-03-29 07:03:09 +01:00
Eva
20e03e1397 client/posts: force file download for download button
So that it works regardless of server configuration.
2025-03-29 07:02:47 +01:00
Eva
a9d870eaa8 client/posts: don't crash on edit mode when no permissions 2025-03-29 07:00:49 +01:00
Eva
08bd3bb890 client/posts: better drag alt text on chrome 2025-03-29 07:00:33 +01:00
Eva
b8b51ded15 client/pools: display error message on merge page when no pool selected
Also fixes a js error.
2025-03-29 06:59:37 +01:00
Eva
f590dc6a41 client/pools, client/tags: obvious button to access posts list 2025-03-29 06:59:22 +01:00
Eva
486fc345fe client/pools, client/tags: move description before details 2025-03-29 06:59:22 +01:00
Eva
ae9e596095 client/notes: remove interaction with main overlay area
Right click Copy would not work on Chrome and Firefox.
Right click Save would not work on Firefox.
2025-03-29 06:58:27 +01:00
Eva
5d1dbb291c client/markdown: allow markdown headers
This regex caught more than was necessary, and in the process made it
impossible to use headers. Only replace instances that will actually
trigger tag permalinks.
2025-03-29 06:58:12 +01:00
Eva
a8b6c143eb client/css: use text color for home footer bullets 2025-03-29 06:57:43 +01:00
Eva
4c6d1a216b client/css: space out similar posts on upload page 2025-03-29 06:57:27 +01:00
Eva
9588d11cb0 client/css: gray out disabled radio button circle 2025-03-29 06:52:46 +01:00
Eva
b08c6eca26 client/comments: prevent avatar focus outline from being clipped 2025-03-29 06:50:56 +01:00
47 changed files with 289 additions and 310 deletions

View File

@ -4,6 +4,9 @@ $comment-header-background-color-darktheme = $top-navigation-color-darktheme
$comment-border-color = #DDD
.comments-container
margin-top: 2em
.comment-container
padding: 0 0 0 60px
@ -16,7 +19,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
@ -114,17 +123,30 @@ $comment-border-color = #DDD
.messages
margin: 1em 0
.darktheme .comment-container .comment header
background: $comment-header-background-color-darktheme
nav.edit
ul
li
&.active
background: $window-color-darktheme
border-bottom: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color-darktheme)
.darktheme .comment-container
.comment
border: 1px solid $comment-header-background-color-darktheme
header
background: $comment-header-background-color-darktheme
border-bottom: 1px solid $comment-header-background-color-darktheme
nav.edit
ul
li
&.active
background: $window-color-darktheme
border: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color-darktheme)
&:before
border-right: 0
&:after
border-right: 0.75em solid $comment-header-background-color-darktheme
.comment-content
p

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

@ -8,11 +8,11 @@ $inactive-tab-text-color-darktheme = $inactive-link-color-darktheme
/* latin */
@font-face
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(../fonts/open_sans.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
font-family: 'Open Sans'
font-style: normal
font-weight: 400
src: local('Open Sans'), local('OpenSans'), url(../fonts/open_sans.woff2) format('woff2')
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000
/* make <body> cover entire viewport */
html
@ -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
@ -161,6 +166,8 @@ nav
li.active a
background: $active-tab-background-color
color: $active-tab-text-color
li.active:has(:focus)
background: $active-tab-background-color
:focus
background: $focused-tab-background-color
outline: 0
@ -236,6 +243,8 @@ nav
li.active a
background: $active-tab-background-color-darktheme
color: $active-tab-text-color-darktheme
li.active:has(:focus)
background: $active-tab-background-color-darktheme
:focus
background: $focused-tab-background-color-darktheme
&#top-navigation

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

@ -130,8 +130,8 @@
background: rgba(255, 0, 0, 0.7)
&:after
color: white
font-family: FontAwesome;
content: "\f1f8"; // fa-trash
font-family: FontAwesome
content: "\f1f8" /* fa-trash */
&:not(.delete)
background: rgba(200, 200, 200, 0.7)
&:after

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

@ -44,30 +44,30 @@ $token-border-color = $active-tab-background-color
.token-flex-container
width: 100%
display: flex;
flex-direction column;
padding-bottom: 0.5em;
display: flex
flex-direction column
padding-bottom: 0.5em
.full-width
width: 100%
.token-flex-row
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.2em;
display: flex
flex-direction: row
justify-content: space-between
padding: 0.2em
.no-wrap
white-space: nowrap;
white-space: nowrap
.token-input
min-height: 2em;
line-height: 2em;
text-align: center;
min-height: 2em
line-height: 2em
text-align: center
.token-flex-column
display: flex;
flex-direction: column;
display: flex
flex-direction: column
.token-flex-labels
padding-right: 0.5em
@ -76,7 +76,7 @@ $token-border-color = $active-tab-background-color
border-top: 3px solid $token-border-color
form
width: 100%;
width: 100%
#user-delete form
width: 100%

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

@ -71,7 +71,7 @@
<% } %>
</td>
<td class='usages'>
<%- tag.postCount %>
<a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>'><%- tag.postCount %></a>
</td>
<td class='creation-time'>
<%= ctx.makeRelativeTime(tag.creationTime) %>

View File

@ -5,9 +5,11 @@ const request = require("superagent");
const events = require("./events.js");
const progress = require("./util/progress.js");
const uri = require("./util/uri.js");
const Info = require("../models/info.js");
let fileTokens = {};
let remoteConfig = null;
let remoteConfigPromise = null;
class Api extends events.EventTarget {
constructor() {
@ -68,9 +70,13 @@ class Api extends events.EventTarget {
fetchConfig() {
if (remoteConfig === null) {
return this.get(uri.formatApiLink("info")).then((response) => {
if (remoteConfigPromise !== null) {
return Promise.resolve(remoteConfigPromise);
}
remoteConfigPromise = Info.get().then((response) => {
remoteConfig = response.config;
});
return remoteConfigPromise;
} else {
return Promise.resolve();
}
@ -398,7 +404,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

@ -2,7 +2,6 @@
const api = require("../api.js");
const config = require("../config.js");
const Info = require("../models/info.js");
const topNavigation = require("../models/top_navigation.js");
const HomeView = require("../views/home_view.js");
@ -20,7 +19,7 @@ class HomeController {
isDevelopmentMode: config.environment == "development",
});
Info.get().then(
api.fetchConfig().then(
(info) => {
this._homeView.setStats({
diskUsage: info.diskUsage,

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]) {
@ -457,6 +460,7 @@ function getTemplate(templatePath) {
makeCssName: misc.makeCssName,
makeNumericInput: makeNumericInput,
formatClientLink: uri.formatClientLink,
escapeTagName: uri.escapeTagName,
});
return htmlToDom(templateFactory(ctx));
};

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

264
client/package-lock.json generated
View File

@ -27,13 +27,19 @@
"html-minifier": "^3.5.18",
"jimp": "^0.13.0",
"pretty-error": "^3.0.3",
"stylus": "^0.54.8",
"stylus": "^0.59.0",
"terser": "^4.8.1",
"underscore": "^1.12.1",
"watchify": "^4.0.0",
"ws": "^7.4.6"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
"dev": true
},
"node_modules/@babel/runtime": {
"version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
@ -516,18 +522,6 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true,
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
@ -1737,27 +1731,6 @@
"node": "*"
}
},
"node_modules/css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"source-map": "^0.6.1",
"source-map-resolve": "^0.5.2",
"urix": "^0.1.0"
}
},
"node_modules/css-parse": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
"integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
"dev": true,
"dependencies": {
"css": "^2.0.0"
}
},
"node_modules/css-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
@ -1795,15 +1768,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/csso": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz",
@ -1830,15 +1794,6 @@
"ms": "2.0.0"
}
},
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"dev": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -3676,13 +3631,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"deprecated": "https://github.com/lydell/resolve-url#deprecated",
"dev": true
},
"node_modules/ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@ -3698,12 +3646,6 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -3794,19 +3736,6 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
"dev": true,
"dependencies": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0",
"resolve-url": "^0.2.1",
"source-map-url": "^0.4.0",
"urix": "^0.1.0"
}
},
"node_modules/source-map-support": {
"version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
@ -3816,12 +3745,6 @@
"source-map": "^0.5.6"
}
},
"node_modules/source-map-url": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true
},
"node_modules/stream-browserify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@ -3912,18 +3835,15 @@
}
},
"node_modules/stylus": {
"version": "0.54.8",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz",
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==",
"version": "0.59.0",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
"integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
"dev": true,
"dependencies": {
"css-parse": "~2.0.0",
"debug": "~3.1.0",
"@adobe/css-tools": "^4.0.1",
"debug": "^4.3.2",
"glob": "^7.1.6",
"mkdirp": "~1.0.4",
"safer-buffer": "^2.1.2",
"sax": "~1.2.4",
"semver": "^6.3.0",
"source-map": "^0.7.3"
},
"bin": {
@ -3931,28 +3851,33 @@
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://opencollective.com/stylus"
}
},
"node_modules/stylus/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"node_modules/stylus/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"bin": {
"mkdirp": "bin/cmd.js"
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=10"
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/stylus/node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
"node_modules/stylus/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/stylus/node_modules/source-map": {
"version": "0.7.3",
@ -4219,13 +4144,6 @@
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true
},
"node_modules/urix": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"deprecated": "Please see https://github.com/lydell/urix#deprecated",
"dev": true
},
"node_modules/url": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
@ -4602,6 +4520,12 @@
}
},
"dependencies": {
"@adobe/css-tools": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
"dev": true
},
"@babel/runtime": {
"version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
@ -5072,12 +4996,6 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
},
"available-typed-arrays": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
@ -6246,35 +6164,6 @@
"randomfill": "^1.0.3"
}
},
"css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"source-map": "^0.6.1",
"source-map-resolve": "^0.5.2",
"urix": "^0.1.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"css-parse": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
"integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
"dev": true,
"requires": {
"css": "^2.0.0"
}
},
"css-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
@ -6326,12 +6215,6 @@
"ms": "2.0.0"
}
},
"decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"dev": true
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -7829,12 +7712,6 @@
"path-parse": "^1.0.6"
}
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
"ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@ -7850,12 +7727,6 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -7931,19 +7802,6 @@
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
},
"source-map-resolve": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
"dev": true,
"requires": {
"atob": "^2.1.2",
"decode-uri-component": "^0.2.0",
"resolve-url": "^0.2.1",
"source-map-url": "^0.4.0",
"urix": "^0.1.0"
}
},
"source-map-support": {
"version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
@ -7953,12 +7811,6 @@
"source-map": "^0.5.6"
}
},
"source-map-url": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true
},
"stream-browserify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@ -8040,31 +7892,31 @@
}
},
"stylus": {
"version": "0.54.8",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz",
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==",
"version": "0.59.0",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
"integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
"dev": true,
"requires": {
"css-parse": "~2.0.0",
"debug": "~3.1.0",
"@adobe/css-tools": "^4.0.1",
"debug": "^4.3.2",
"glob": "^7.1.6",
"mkdirp": "~1.0.4",
"safer-buffer": "^2.1.2",
"sax": "~1.2.4",
"semver": "^6.3.0",
"source-map": "^0.7.3"
},
"dependencies": {
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"source-map": {
@ -8287,12 +8139,6 @@
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true
},
"urix": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"dev": true
},
"url": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",

View File

@ -28,7 +28,7 @@
"html-minifier": "^3.5.18",
"jimp": "^0.13.0",
"pretty-error": "^3.0.3",
"stylus": "^0.54.8",
"stylus": "^0.59.0",
"terser": "^4.8.1",
"underscore": "^1.12.1",
"watchify": "^4.0.0",

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"]
)
@ -161,7 +161,9 @@ def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
posts.update_post_notes(post, ctx.get_param_as_list("notes"))
if ctx.has_param("flags"):
auth.verify_privilege(ctx.user, "posts:edit:flags")
posts.update_post_flags(post, ctx.get_param_as_string_list("flags"))
posts.update_post_flags(post, ctx.get_param_as_string_list("flags"), remove=True)
else:
posts.update_post_flags(post, post.flags, remove=True)
if ctx.has_file("thumbnail"):
auth.verify_privilege(ctx.user, "posts:edit:thumbnail")
posts.update_post_thumbnail(post, ctx.get_file("thumbnail"))

View File

@ -30,7 +30,7 @@ def has_favorited(entity: model.Base, user: model.User) -> bool:
return _get_fav_entity(entity, user) is not None
def unset_favorite(entity: model.Base, user: Optional[model.User]) -> None:
def unset_favorite(entity: model.Base, user: model.User) -> None:
assert entity
assert user
fav_entity = _get_fav_entity(entity, user)
@ -38,7 +38,7 @@ def unset_favorite(entity: model.Base, user: Optional[model.User]) -> None:
db.session.delete(fav_entity)
def set_favorite(entity: model.Base, user: Optional[model.User]) -> None:
def set_favorite(entity: model.Base, user: model.User) -> None:
from szurubooru.func import scores
assert entity

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

@ -94,6 +94,7 @@ TYPE_MAP = {
FLAG_MAP = {
model.Post.FLAG_LOOP: "loop",
model.Post.FLAG_SOUND: "sound",
model.Post.FLAG_TAGME: "tagme",
}
@ -264,8 +265,8 @@ class PostSerializer(serialization.BaseSerializer):
{
post["id"]: post
for post in [
serialize_micro_post(rel, self.auth_user)
for rel in self.post.relations
serialize_micro_post(try_get_post_by_id(rel.child_id), self.auth_user)
for rel in get_post_relations(self.post.post_id)
]
}.values(),
key=lambda post: post["id"],
@ -281,16 +282,14 @@ class PostSerializer(serialization.BaseSerializer):
return scores.get_score(self.post, self.auth_user)
def serialize_own_favorite(self) -> Any:
return (
len(
[
user
for user in self.post.favorited_by
if user.user_id == self.auth_user.user_id
]
)
> 0
)
if self.auth_user.user_id is None:
return False
for user in self.post.favorited_by:
if user.user_id == self.auth_user.user_id:
return True
return False
def serialize_tag_count(self) -> Any:
return self.post.tag_count
@ -365,6 +364,10 @@ def get_post_count() -> int:
return db.session.query(sa.func.count(model.Post.post_id)).one()[0]
def get_post_relations(post_id: int) -> List[model.Post]:
return db.session.query(model.PostRelation).filter(model.PostRelation.parent_id == post_id).all()
def try_get_post_by_id(post_id: int) -> Optional[model.Post]:
return (
db.session.query(model.Post)
@ -531,10 +534,13 @@ 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)
ret.append(model.Post.FLAG_TAGME)
return ret
@ -779,10 +785,12 @@ def update_post_notes(post: model.Post, notes: Any) -> None:
)
def update_post_flags(post: model.Post, flags: List[str]) -> None:
def update_post_flags(post: model.Post, flags: List[str], remove: bool = False) -> None:
assert post
target_flags = []
for flag in flags:
if remove and flag == model.Post.FLAG_TAGME:
continue
flag = util.flip(FLAG_MAP).get(flag, None)
if not flag:
raise InvalidPostFlagError(

View File

@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, Optional
import sqlalchemy as sa
from szurubooru import db, model
from szurubooru.func import diff, net, users
from szurubooru.func import diff, net, users, posts
def get_tag_category_snapshot(category: model.TagCategory) -> Dict[str, Any]:
@ -53,7 +53,7 @@ def get_post_snapshot(post: model.Post) -> Dict[str, Any]:
"flags": post.flags,
"featured": post.is_featured,
"tags": sorted([tag.first_name for tag in post.tags]),
"relations": sorted([rel.post_id for rel in post.relations]),
"relations": sorted([rel.child_id for rel in posts.get_post_relations(post.post_id)]),
"notes": sorted(
[
{

View File

@ -197,6 +197,7 @@ class Post(Base):
FLAG_LOOP = "loop"
FLAG_SOUND = "sound"
FLAG_TAGME = "tagme"
# basic meta
post_id = sa.Column("id", sa.Integer, primary_key=True)

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)
@ -43,6 +47,7 @@ def _flag_transformer(value: str) -> str:
available_values = {
"loop": model.Post.FLAG_LOOP,
"sound": model.Post.FLAG_SOUND,
"tagme": model.Post.FLAG_TAGME,
}
return "%" + search_util.enum_transformer(available_values, value) + "%"

View File

@ -70,7 +70,7 @@ def test_creating_minimal_posts(context_factory, post_factory, user_factory):
posts.update_post_source.assert_called_once_with(post, "")
posts.update_post_relations.assert_called_once_with(post, [])
posts.update_post_notes.assert_called_once_with(post, [])
posts.update_post_flags.assert_called_once_with(post, [])
posts.update_post_flags.assert_called_once_with(post, ["tagme"])
posts.update_post_thumbnail.assert_called_once_with(
post, "post-thumbnail"
)

View File

@ -93,7 +93,7 @@ def test_post_updating(
post, ["note1", "note2"]
)
posts.update_post_flags.assert_called_once_with(
post, ["flag1", "flag2"]
post, ["flag1", "flag2"], remove=True
)
posts.serialize_post.assert_called_once_with(
post, auth_user, options=[]