This commit is contained in:
Eva
2025-04-03 01:17:40 +00:00
committed by GitHub
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 $comment-border-color = #DDD
.comments-container
margin-top: 2em
.comment-container .comment-container
padding: 0 0 0 60px padding: 0 0 0 60px
@ -16,7 +19,13 @@ $comment-border-color = #DDD
width: 40px width: 40px
height: 40px height: 40px
a a
outline: none
display: inline-block display: inline-block
position: relative
top: -2px
border: 2px solid transparent
&:focus
border: 2px solid $main-color
nav:not(.active), .tab:not(.active) nav:not(.active), .tab:not(.active)
display: none display: none
@ -114,18 +123,31 @@ $comment-border-color = #DDD
.messages .messages
margin: 1em 0 margin: 1em 0
.darktheme .comment-container .comment header .darktheme .comment-container
.comment
border: 1px solid $comment-header-background-color-darktheme
header
background: $comment-header-background-color-darktheme background: $comment-header-background-color-darktheme
border-bottom: 1px solid $comment-header-background-color-darktheme
nav.edit nav.edit
ul ul
li li
&.active &.active
background: $window-color-darktheme background: $window-color-darktheme
border-bottom: 1px solid $window-color-darktheme border: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a .edit, .delete, .score-container a, .nickname a
&:not(.inactive) &:not(.inactive)
color: mix($main-color, $inactive-link-color-darktheme) 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 .comment-content
p p
word-wrap: normal word-wrap: normal

View File

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

View File

@ -8,11 +8,11 @@ $inactive-tab-text-color-darktheme = $inactive-link-color-darktheme
/* latin */ /* latin */
@font-face @font-face
font-family: 'Open Sans'; font-family: 'Open Sans'
font-style: normal; font-style: normal
font-weight: 400; font-weight: 400
src: local('Open Sans'), local('OpenSans'), url(../fonts/open_sans.woff2) format('woff2'); 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; 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 */ /* make <body> cover entire viewport */
html html
@ -106,7 +106,12 @@ form .fa-question-circle-o
background-color: $scrollbar-bg-color background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb &::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color 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 background: $top-navigation-color
padding: 1.8em padding: 1.8em
@media (max-width: 1000px) @media (max-width: 1000px)
@ -117,7 +122,7 @@ form .fa-question-circle-o
margin-bottom: 0 margin-bottom: 0
.darktheme #content-holder .darktheme #content-holder
>.content-wrapper:not(.transparent) >.content-wrapper:not(.transparent-container)
background: $top-navigation-color-darktheme background: $top-navigation-color-darktheme
hr hr
@ -161,6 +166,8 @@ nav
li.active a li.active a
background: $active-tab-background-color background: $active-tab-background-color
color: $active-tab-text-color color: $active-tab-text-color
li.active:has(:focus)
background: $active-tab-background-color
:focus :focus
background: $focused-tab-background-color background: $focused-tab-background-color
outline: 0 outline: 0
@ -236,6 +243,8 @@ nav
li.active a li.active a
background: $active-tab-background-color-darktheme background: $active-tab-background-color-darktheme
color: $active-tab-text-color-darktheme color: $active-tab-text-color-darktheme
li.active:has(:focus)
background: $active-tab-background-color-darktheme
:focus :focus
background: $focused-tab-background-color-darktheme background: $focused-tab-background-color-darktheme
&#top-navigation &#top-navigation

View File

@ -61,7 +61,10 @@
word-spacing: 1.1em word-spacing: 1.1em
background-repeat: no-repeat background-repeat: no-repeat
background-position: 50% 50% 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 .thumbnail
margin-right: 0.4em 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) background: rgba(255, 0, 0, 0.7)
&:after &:after
color: white color: white
font-family: FontAwesome; font-family: FontAwesome
content: "\f1f8"; // fa-trash content: "\f1f8" /* fa-trash */
&:not(.delete) &:not(.delete)
background: rgba(200, 200, 200, 0.7) background: rgba(200, 200, 200, 0.7)
&:after &:after

View File

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

View File

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

View File

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

View File

@ -34,6 +34,16 @@ shortcuts:</p>
<td>Focus first post in post list</td> <td>Focus first post in post list</td>
</tr> </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> <tr>
<td><kbd>Delete</kbd></td> <td><kbd>Delete</kbd></td>
<td>Delete post (while in edit mode)</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> <div class='messages'></div>
<header> <header>
<h1><%- ctx.name %></h1> <h1><%- ctx.name %></h1>

View File

@ -2,6 +2,7 @@
<h1><%- ctx.getPrettyName(ctx.pool.names[0]) %></h1> <h1><%- ctx.getPrettyName(ctx.pool.names[0]) %></h1>
<nav class='buttons'><!-- <nav class='buttons'><!--
--><ul><!-- --><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><!-- --><li data-name='summary'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!-- --><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'edit') %>'>Edit</a></li><!-- --><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'> <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 class='details'>
<section> <section>
Category: Category:
@ -14,10 +20,4 @@
--></ul> --></ul>
</section> </section>
</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> </div>

View File

@ -1,7 +1,7 @@
<div class='post-content post-type-<%- ctx.post.type %>'> <div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(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') { %> <% } 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'> <aside class='sidebar'>
<nav class='buttons'> <nav class='buttons'>
<article class='previous-post'> <article class='previous-post'>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
<h1><%- ctx.getPrettyName(ctx.tag.names[0]) %></h1> <h1><%- ctx.getPrettyName(ctx.tag.names[0]) %></h1>
<nav class='buttons'><!-- <nav class='buttons'><!--
--><ul><!-- --><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><!-- --><li data-name='summary'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0]) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!-- --><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'edit') %>'>Edit</a></li><!-- --><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'> <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 class='details'>
<section> <section>
Category: Category:
@ -32,10 +38,4 @@
--></ul> --></ul>
</section> </section>
</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> </div>

View File

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

View File

@ -5,9 +5,11 @@ const request = require("superagent");
const events = require("./events.js"); const events = require("./events.js");
const progress = require("./util/progress.js"); const progress = require("./util/progress.js");
const uri = require("./util/uri.js"); const uri = require("./util/uri.js");
const Info = require("../models/info.js");
let fileTokens = {}; let fileTokens = {};
let remoteConfig = null; let remoteConfig = null;
let remoteConfigPromise = null;
class Api extends events.EventTarget { class Api extends events.EventTarget {
constructor() { constructor() {
@ -68,9 +70,13 @@ class Api extends events.EventTarget {
fetchConfig() { fetchConfig() {
if (remoteConfig === null) { 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; remoteConfig = response.config;
}); });
return remoteConfigPromise;
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
@ -398,7 +404,7 @@ class Api extends events.EventTarget {
if (data) { if (data) {
if (files && Object.keys(files).length) { 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 { } else {
req.set("Content-Type", "application/json"); req.set("Content-Type", "application/json");
req.send(data); req.send(data);

View File

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

View File

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

View File

@ -4,6 +4,7 @@ const api = require("../api.js");
const events = require("../events.js"); const events = require("../events.js");
const misc = require("../util/misc.js"); const misc = require("../util/misc.js");
const views = require("../util/views.js"); const views = require("../util/views.js");
const keyboard = require("../util/keyboard.js");
const Note = require("../models/note.js"); const Note = require("../models/note.js");
const Point = require("../models/point.js"); const Point = require("../models/point.js");
const TagInputControl = require("./tag_input_control.js"); const TagInputControl = require("./tag_input_control.js");
@ -224,10 +225,12 @@ class PostEditSidebarControl extends events.EventTarget {
}); });
} }
if (this._tagControl) {
this._tagControl.addEventListener("change", (e) => { this._tagControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change")); this.dispatchEvent(new CustomEvent("change"));
this._syncExpanderTitles(); this._syncExpanderTitles();
}); });
}
if (this._noteTextareaNode) { if (this._noteTextareaNode) {
this._noteTextareaNode.addEventListener("change", (e) => this._noteTextareaNode.addEventListener("change", (e) =>
@ -241,6 +244,16 @@ class PostEditSidebarControl extends events.EventTarget {
this._syncExpanderTitles(); 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() { _syncExpanderTitles() {

View File

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

View File

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

View File

@ -40,6 +40,11 @@ class Settings extends events.EventTarget {
save(newSettings, silent) { save(newSettings, silent) {
newSettings = Object.assign(this.cache, newSettings); newSettings = Object.assign(this.cache, newSettings);
localStorage.setItem("settings", JSON.stringify(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(); this.cache = this._getFromLocalStorage();
if (silent !== true) { if (silent !== true) {
this.dispatchEvent( this.dispatchEvent(

View File

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

View File

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

View File

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

View File

@ -111,7 +111,7 @@ function makeSelect(options) {
} }
function makeInput(options) { function makeInput(options) {
options.value = options.value || ""; options.value = options.value === 0 ? 0 : options.value || "";
return _makeLabel(options) + makeElement("input", options); return _makeLabel(options) + makeElement("input", options);
} }
@ -261,7 +261,7 @@ function makePoolLink(id, includeHash, includeCount, pool, name) {
} }
function makeUserLink(user) { 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"; text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
const link = const link =
user && user.name && api.hasPrivilege("users:view") user && user.name && api.hasPrivilege("users:view")
@ -300,6 +300,8 @@ function _serializeElement(name, attributes) {
attributes[key] === undefined attributes[key] === undefined
) { ) {
return ""; return "";
} else if (attributes[key] === 0) {
return `${key}="0"`;
} }
const attribute = misc.escapeHtml(attributes[key] || ""); const attribute = misc.escapeHtml(attributes[key] || "");
return `${key}="${attribute}"`; return `${key}="${attribute}"`;
@ -321,6 +323,7 @@ function emptyContent(target) {
} }
function replaceContent(target, source) { function replaceContent(target, source) {
if (!target) return;
emptyContent(target); emptyContent(target);
if (source instanceof NodeList) { if (source instanceof NodeList) {
for (let child of [...source]) { for (let child of [...source]) {
@ -457,6 +460,7 @@ function getTemplate(templatePath) {
makeCssName: misc.makeCssName, makeCssName: misc.makeCssName,
makeNumericInput: makeNumericInput, makeNumericInput: makeNumericInput,
formatClientLink: uri.formatClientLink, formatClientLink: uri.formatClientLink,
escapeTagName: uri.escapeTagName,
}); });
return htmlToDom(templateFactory(ctx)); return htmlToDom(templateFactory(ctx));
}; };

View File

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

View File

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

View File

@ -29,7 +29,7 @@ function _formatBasicChange(diff, text) {
return lines; return lines;
} }
function _makeResourceLink(type, id) { function _makeResourceLink(type, id, data) {
if (type === "post") { if (type === "post") {
return views.makePostLink(id, true); return views.makePostLink(id, true);
} else if (type === "tag") { } else if (type === "tag") {
@ -37,7 +37,7 @@ function _makeResourceLink(type, id) {
} else if (type === "tag_category") { } else if (type === "tag_category") {
return 'category "' + id + '"'; return 'category "' + id + '"';
} else if (type === "pool") { } 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", "html-minifier": "^3.5.18",
"jimp": "^0.13.0", "jimp": "^0.13.0",
"pretty-error": "^3.0.3", "pretty-error": "^3.0.3",
"stylus": "^0.54.8", "stylus": "^0.59.0",
"terser": "^4.8.1", "terser": "^4.8.1",
"underscore": "^1.12.1", "underscore": "^1.12.1",
"watchify": "^4.0.0", "watchify": "^4.0.0",
"ws": "^7.4.6" "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": { "node_modules/@babel/runtime": {
"version": "7.10.3", "version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz", "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", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "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": { "node_modules/available-typed-arrays": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
@ -1737,27 +1731,6 @@
"node": "*" "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": { "node_modules/css-select": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
@ -1795,15 +1768,6 @@
"url": "https://github.com/sponsors/fb55" "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": { "node_modules/csso": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz",
@ -1830,15 +1794,6 @@
"ms": "2.0.0" "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": { "node_modules/define-properties": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -3676,13 +3631,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/ripemd160": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "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", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" "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": { "node_modules/sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -3794,19 +3736,6 @@
"node": ">=0.10.0" "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": { "node_modules/source-map-support": {
"version": "0.4.18", "version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
@ -3816,12 +3745,6 @@
"source-map": "^0.5.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": { "node_modules/stream-browserify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@ -3912,18 +3835,15 @@
} }
}, },
"node_modules/stylus": { "node_modules/stylus": {
"version": "0.54.8", "version": "0.59.0",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"css-parse": "~2.0.0", "@adobe/css-tools": "^4.0.1",
"debug": "~3.1.0", "debug": "^4.3.2",
"glob": "^7.1.6", "glob": "^7.1.6",
"mkdirp": "~1.0.4",
"safer-buffer": "^2.1.2",
"sax": "~1.2.4", "sax": "~1.2.4",
"semver": "^6.3.0",
"source-map": "^0.7.3" "source-map": "^0.7.3"
}, },
"bin": { "bin": {
@ -3931,28 +3851,33 @@
}, },
"engines": { "engines": {
"node": "*" "node": "*"
},
"funding": {
"url": "https://opencollective.com/stylus"
} }
}, },
"node_modules/stylus/node_modules/mkdirp": { "node_modules/stylus/node_modules/debug": {
"version": "1.0.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true, "dev": true,
"bin": { "dependencies": {
"mkdirp": "bin/cmd.js" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
} }
}, },
"node_modules/stylus/node_modules/semver": { "node_modules/stylus/node_modules/ms": {
"version": "6.3.0", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true, "dev": true
"bin": {
"semver": "bin/semver.js"
}
}, },
"node_modules/stylus/node_modules/source-map": { "node_modules/stylus/node_modules/source-map": {
"version": "0.7.3", "version": "0.7.3",
@ -4219,13 +4144,6 @@
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true "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": { "node_modules/url": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
@ -4602,6 +4520,12 @@
} }
}, },
"dependencies": { "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": { "@babel/runtime": {
"version": "7.10.3", "version": "7.10.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz", "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", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "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": { "available-typed-arrays": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
@ -6246,35 +6164,6 @@
"randomfill": "^1.0.3" "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": { "css-select": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
@ -6326,12 +6215,6 @@
"ms": "2.0.0" "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": { "define-properties": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -7829,12 +7712,6 @@
"path-parse": "^1.0.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": { "ripemd160": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "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", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" "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": { "sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@ -7931,19 +7802,6 @@
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true "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": { "source-map-support": {
"version": "0.4.18", "version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", "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": "^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": { "stream-browserify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@ -8040,31 +7892,31 @@
} }
}, },
"stylus": { "stylus": {
"version": "0.54.8", "version": "0.59.0",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
"dev": true, "dev": true,
"requires": { "requires": {
"css-parse": "~2.0.0", "@adobe/css-tools": "^4.0.1",
"debug": "~3.1.0", "debug": "^4.3.2",
"glob": "^7.1.6", "glob": "^7.1.6",
"mkdirp": "~1.0.4",
"safer-buffer": "^2.1.2",
"sax": "~1.2.4", "sax": "~1.2.4",
"semver": "^6.3.0",
"source-map": "^0.7.3" "source-map": "^0.7.3"
}, },
"dependencies": { "dependencies": {
"mkdirp": { "debug": {
"version": "1.0.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true "dev": true,
"requires": {
"ms": "2.1.2"
}
}, },
"semver": { "ms": {
"version": "6.3.0", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"source-map": { "source-map": {
@ -8287,12 +8139,6 @@
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true "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": { "url": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",

View File

@ -28,7 +28,7 @@
"html-minifier": "^3.5.18", "html-minifier": "^3.5.18",
"jimp": "^0.13.0", "jimp": "^0.13.0",
"pretty-error": "^3.0.3", "pretty-error": "^3.0.3",
"stylus": "^0.54.8", "stylus": "^0.59.0",
"terser": "^4.8.1", "terser": "^4.8.1",
"underscore": "^1.12.1", "underscore": "^1.12.1",
"watchify": "^4.0.0", "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: def _get_post_id(params: Dict[str, str]) -> int:
try: try:
return int(params["post_id"]) return int(params["post_id"])
except TypeError: except (TypeError, ValueError):
raise posts.InvalidPostIdError( raise posts.InvalidPostIdError(
"Invalid post ID: %r." % params["post_id"] "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")) posts.update_post_notes(post, ctx.get_param_as_list("notes"))
if ctx.has_param("flags"): if ctx.has_param("flags"):
auth.verify_privilege(ctx.user, "posts:edit: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"): if ctx.has_file("thumbnail"):
auth.verify_privilege(ctx.user, "posts:edit:thumbnail") auth.verify_privilege(ctx.user, "posts:edit:thumbnail")
posts.update_post_thumbnail(post, ctx.get_file("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 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 entity
assert user assert user
fav_entity = _get_fav_entity(entity, 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) 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 from szurubooru.func import scores
assert entity assert entity

View File

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

View File

@ -94,6 +94,7 @@ TYPE_MAP = {
FLAG_MAP = { FLAG_MAP = {
model.Post.FLAG_LOOP: "loop", model.Post.FLAG_LOOP: "loop",
model.Post.FLAG_SOUND: "sound", model.Post.FLAG_SOUND: "sound",
model.Post.FLAG_TAGME: "tagme",
} }
@ -264,8 +265,8 @@ class PostSerializer(serialization.BaseSerializer):
{ {
post["id"]: post post["id"]: post
for post in [ for post in [
serialize_micro_post(rel, self.auth_user) serialize_micro_post(try_get_post_by_id(rel.child_id), self.auth_user)
for rel in self.post.relations for rel in get_post_relations(self.post.post_id)
] ]
}.values(), }.values(),
key=lambda post: post["id"], key=lambda post: post["id"],
@ -281,16 +282,14 @@ class PostSerializer(serialization.BaseSerializer):
return scores.get_score(self.post, self.auth_user) return scores.get_score(self.post, self.auth_user)
def serialize_own_favorite(self) -> Any: def serialize_own_favorite(self) -> Any:
return ( if self.auth_user.user_id is None:
len( return False
[
user for user in self.post.favorited_by:
for user in self.post.favorited_by if user.user_id == self.auth_user.user_id:
if user.user_id == self.auth_user.user_id return True
]
) return False
> 0
)
def serialize_tag_count(self) -> Any: def serialize_tag_count(self) -> Any:
return self.post.tag_count 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] 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]: def try_get_post_by_id(post_id: int) -> Optional[model.Post]:
return ( return (
db.session.query(model.Post) db.session.query(model.Post)
@ -531,10 +534,13 @@ def generate_alternate_formats(
def get_default_flags(content: bytes) -> List[str]: def get_default_flags(content: bytes) -> List[str]:
assert content assert content
ret = [] ret = []
if mime.is_video(mime.get_mime_type(content)): if mime.is_animated_gif(content):
if images.check_for_loop(content):
ret.append(model.Post.FLAG_LOOP) ret.append(model.Post.FLAG_LOOP)
elif mime.is_video(mime.get_mime_type(content)):
if images.Image(content).check_for_sound(): if images.Image(content).check_for_sound():
ret.append(model.Post.FLAG_SOUND) ret.append(model.Post.FLAG_SOUND)
ret.append(model.Post.FLAG_TAGME)
return ret 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 assert post
target_flags = [] target_flags = []
for flag in flags: for flag in flags:
if remove and flag == model.Post.FLAG_TAGME:
continue
flag = util.flip(FLAG_MAP).get(flag, None) flag = util.flip(FLAG_MAP).get(flag, None)
if not flag: if not flag:
raise InvalidPostFlagError( raise InvalidPostFlagError(

View File

@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, Optional
import sqlalchemy as sa import sqlalchemy as sa
from szurubooru import db, model 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]: 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, "flags": post.flags,
"featured": post.is_featured, "featured": post.is_featured,
"tags": sorted([tag.first_name for tag in post.tags]), "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( "notes": sorted(
[ [
{ {

View File

@ -197,6 +197,7 @@ class Post(Base):
FLAG_LOOP = "loop" FLAG_LOOP = "loop"
FLAG_SOUND = "sound" FLAG_SOUND = "sound"
FLAG_TAGME = "tagme"
# basic meta # basic meta
post_id = sa.Column("id", sa.Integer, primary_key=True) 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: def _safety_transformer(value: str) -> str:
available_values = { available_values = {
"safe": model.Post.SAFETY_SAFE, "safe": model.Post.SAFETY_SAFE,
"s": model.Post.SAFETY_SAFE,
"sketchy": model.Post.SAFETY_SKETCHY, "sketchy": model.Post.SAFETY_SKETCHY,
"questionable": model.Post.SAFETY_SKETCHY, "questionable": model.Post.SAFETY_SKETCHY,
"q": model.Post.SAFETY_SKETCHY,
"unsafe": model.Post.SAFETY_UNSAFE, "unsafe": model.Post.SAFETY_UNSAFE,
"explicit": model.Post.SAFETY_UNSAFE,
"e": model.Post.SAFETY_UNSAFE,
} }
return search_util.enum_transformer(available_values, value) return search_util.enum_transformer(available_values, value)
@ -43,6 +47,7 @@ def _flag_transformer(value: str) -> str:
available_values = { available_values = {
"loop": model.Post.FLAG_LOOP, "loop": model.Post.FLAG_LOOP,
"sound": model.Post.FLAG_SOUND, "sound": model.Post.FLAG_SOUND,
"tagme": model.Post.FLAG_TAGME,
} }
return "%" + search_util.enum_transformer(available_values, value) + "%" 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_source.assert_called_once_with(post, "")
posts.update_post_relations.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_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( posts.update_post_thumbnail.assert_called_once_with(
post, "post-thumbnail" post, "post-thumbnail"
) )

View File

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