This commit is contained in:
Eva
2025-03-31 23:49:06 +00:00
committed by GitHub
36 changed files with 172 additions and 55 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,17 +123,30 @@ $comment-border-color = #DDD
.messages .messages
margin: 1em 0 margin: 1em 0
.darktheme .comment-container .comment header .darktheme .comment-container
background: $comment-header-background-color-darktheme .comment
nav.edit border: 1px solid $comment-header-background-color-darktheme
ul
li header
&.active background: $comment-header-background-color-darktheme
background: $window-color-darktheme border-bottom: 1px solid $comment-header-background-color-darktheme
border-bottom: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a nav.edit
&:not(.inactive) ul
color: mix($main-color, $inactive-link-color-darktheme) 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 .comment-content
p p

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

@ -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

@ -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

@ -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

@ -398,7 +398,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

@ -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);
} }
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) => { 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 {
}); });
} }
this._tagControl.addEventListener("change", (e) => { if (this._tagControl) {
this.dispatchEvent(new CustomEvent("change")); this._tagControl.addEventListener("change", (e) => {
this._syncExpanderTitles(); this.dispatchEvent(new CustomEvent("change"));
}); 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]) {

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

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"]
) )

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

@ -531,8 +531,10 @@ 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):
ret.append(model.Post.FLAG_LOOP) 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(): if images.Image(content).check_for_sound():
ret.append(model.Post.FLAG_SOUND) ret.append(model.Post.FLAG_SOUND)
return ret return ret

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)