mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Compare commits
61 Commits
dependabot
...
c705e9b9f1
Author | SHA1 | Date | |
---|---|---|---|
c705e9b9f1 | |||
9f533882bf | |||
6035278c7e | |||
acb1eddb53 | |||
b3fa26b3fb | |||
d3c2da4f91 | |||
8acd6ab776 | |||
2ed19e013a | |||
2b06e1cafa | |||
7f6211e0cc | |||
86bbd429f7 | |||
0b02826e6d | |||
ea215901af | |||
9884161297 | |||
b2501b7ee2 | |||
7f2a8b5b07 | |||
de9b8fdce3 | |||
ae85605531 | |||
ae72d75631 | |||
ce613c5ade | |||
e05252f67e | |||
20e03e1397 | |||
a9d870eaa8 | |||
08bd3bb890 | |||
b8b51ded15 | |||
f590dc6a41 | |||
486fc345fe | |||
ae9e596095 | |||
5d1dbb291c | |||
a8b6c143eb | |||
4c6d1a216b | |||
9588d11cb0 | |||
b08c6eca26 | |||
376f687c38 | |||
4fd848abf2 | |||
61b9f81e39 | |||
b721865931 | |||
46e3295003 | |||
031131506e | |||
d102578b54 | |||
6edb25d87b | |||
93fc15f2a4 | |||
4f9d46e1c2 | |||
b72e81850d | |||
c1c695f082 | |||
4b6b231fc8 | |||
6b0c3cfc7f | |||
4ec8cb3ba2 | |||
8d971234a2 | |||
a16bb198ab | |||
3f182a66ad | |||
b52363e82d | |||
3bf45e4c0a | |||
5596f53744 | |||
da425afc49 | |||
d7394d672f | |||
190d795426 | |||
7c92ceaf6a | |||
9e00f37464 | |||
59c497e168 | |||
c292b96f06 |
@ -8,7 +8,7 @@ scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||
## Features
|
||||
|
||||
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
|
||||
- Ability to retrieve web video content using [youtube-dl](https://github.com/ytdl-org/youtube-dl)
|
||||
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||
- Post comments
|
||||
- Post notes / annotations, including arbitrary polygons
|
||||
- Rich JSON REST API ([see documentation](doc/API.md))
|
||||
|
@ -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,19 +123,36 @@ $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
|
||||
word-wrap: normal
|
||||
word-break: break-word
|
||||
|
||||
ul, ol
|
||||
list-style-position: inside
|
||||
margin: 1em 0
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -300,10 +309,10 @@ a .access-key
|
||||
background-size: 20px 20px
|
||||
img
|
||||
opacity: 0
|
||||
width: auto
|
||||
width: 100%
|
||||
height: 100%
|
||||
video
|
||||
width: auto
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
.flexbox-dummy
|
||||
|
@ -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>")
|
@ -187,6 +187,9 @@
|
||||
vertical-align: top
|
||||
@media (max-width: 1000px)
|
||||
display: block
|
||||
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
|
||||
float: left
|
||||
margin-right: 1em
|
||||
input
|
||||
margin-bottom: 0.25em
|
||||
margin-right: 0.25em
|
||||
|
@ -15,38 +15,42 @@
|
||||
border: 0
|
||||
outline: 0
|
||||
|
||||
nav.buttons
|
||||
margin-top: 0
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
article
|
||||
flex: 1 0 33%
|
||||
a
|
||||
display: inline-block
|
||||
width: 100%
|
||||
padding: 0.3em 0
|
||||
text-align: center
|
||||
vertical-align: middle
|
||||
transition: background 0.2s linear, box-shadow 0.2s linear
|
||||
&:not(.inactive):hover
|
||||
background: lighten($main-color, 90%)
|
||||
i
|
||||
font-size: 140%
|
||||
>.sidebar>nav.buttons, >.content nav.buttons
|
||||
margin-top: 0
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
article
|
||||
flex: 1 0 33%
|
||||
a
|
||||
display: inline-block
|
||||
width: 100%
|
||||
padding: 0.3em 0
|
||||
text-align: center
|
||||
@media (max-width: 800px)
|
||||
margin-top: 2em
|
||||
vertical-align: middle
|
||||
transition: background 0.2s linear, box-shadow 0.2s linear
|
||||
&:not(.inactive):hover
|
||||
background: lighten($main-color, 90%)
|
||||
i
|
||||
font-size: 140%
|
||||
text-align: center
|
||||
@media (max-width: 800px)
|
||||
margin-top: 0.6em
|
||||
margin-bottom: 0.6em
|
||||
|
||||
>.content
|
||||
width: 100%
|
||||
|
||||
.post-container
|
||||
margin-bottom: 2em
|
||||
margin-bottom: 0.6em
|
||||
|
||||
.post-content
|
||||
margin: 0
|
||||
|
||||
.after-mobile-controls
|
||||
width: 100%
|
||||
|
||||
.darktheme .post-view
|
||||
>.sidebar
|
||||
>.sidebar, >.content
|
||||
nav.buttons
|
||||
article
|
||||
a:not(.inactive):hover
|
||||
@ -56,6 +60,8 @@
|
||||
@media (max-width: 800px)
|
||||
.post-view
|
||||
flex-wrap: wrap
|
||||
>.after-mobile-controls
|
||||
order: 3
|
||||
>.sidebar
|
||||
order: 2
|
||||
min-width: 100%
|
||||
@ -113,7 +119,6 @@
|
||||
h1
|
||||
margin-bottom: 0.5em
|
||||
.thumbnail
|
||||
background-position: 50% 30%
|
||||
width: 4em
|
||||
height: 3em
|
||||
li
|
||||
|
@ -18,6 +18,8 @@
|
||||
right: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
&[data-state=read-only]
|
||||
pointer-events: none
|
||||
|
||||
.notes-overlay
|
||||
g
|
||||
|
@ -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
|
||||
|
@ -21,10 +21,11 @@
|
||||
.details
|
||||
font-size: 90%
|
||||
line-height: 130%
|
||||
.image
|
||||
margin: 0.25em 0.6em 0.25em 0
|
||||
.thumbnail
|
||||
width: 3em
|
||||
height: 3em
|
||||
margin: 0.25em 0.6em 0 0
|
||||
|
||||
.darktheme .user-list
|
||||
ul li
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<meta name='theme-color' content='#24aadd'/>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'/>
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
|
||||
|
@ -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><!--
|
||||
|
@ -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>
|
||||
|
@ -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') { %>
|
||||
|
||||
|
@ -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'>
|
||||
@ -29,6 +29,7 @@
|
||||
<span class='vim-nav-hint'>Next post ></span>
|
||||
</a>
|
||||
</article>
|
||||
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
||||
<article class='edit-post'>
|
||||
<% if (ctx.editMode) { %>
|
||||
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
@ -36,16 +37,13 @@
|
||||
<span class='vim-nav-hint'>Back to view mode</span>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
||||
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
<% } else { %>
|
||||
<a class='inactive'>
|
||||
<% } %>
|
||||
<i class='fa fa-pencil'></i>
|
||||
<span class='vim-nav-hint'>Edit post</span>
|
||||
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
<i class='fa fa-pencil'></i>
|
||||
<span class='vim-nav-hint'>Edit post</span>
|
||||
</a>
|
||||
<% } %>
|
||||
</article>
|
||||
<% } %>
|
||||
</nav>
|
||||
|
||||
<div class='sidebar-container'></div>
|
||||
@ -54,13 +52,15 @@
|
||||
<div class='content'>
|
||||
<div class='post-container'></div>
|
||||
|
||||
<% if (ctx.canListComments) { %>
|
||||
<div class='comments-container'></div>
|
||||
<% } %>
|
||||
<div class='after-mobile-controls'>
|
||||
<% if (ctx.canCreateComments) { %>
|
||||
<h2>Add comment</h2>
|
||||
<div class='comment-form-container'></div>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canCreateComments) { %>
|
||||
<h2>Add comment</h2>
|
||||
<div class='comment-form-container'></div>
|
||||
<% } %>
|
||||
<% if (ctx.canListComments) { %>
|
||||
<div class='comments-container'></div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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) %> <!--
|
||||
--><%- {
|
||||
@ -17,8 +17,8 @@
|
||||
'video/mp4': 'MPEG-4',
|
||||
'video/quicktime': 'MOV',
|
||||
'application/x-shockwave-flash': 'SWF',
|
||||
}[ctx.post.mimeType] %>
|
||||
</a>
|
||||
}[ctx.post.mimeType] %><!--
|
||||
--></a>
|
||||
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
||||
<% if (ctx.post.flags.length) { %><!--
|
||||
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
|
||||
@ -58,7 +58,7 @@
|
||||
Search on
|
||||
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> ·
|
||||
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> ·
|
||||
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||
</section>
|
||||
|
||||
<section class='social'>
|
||||
@ -99,10 +99,10 @@
|
||||
--><% if (ctx.canListPosts) { %><!--
|
||||
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
||||
--><% } %><!--
|
||||
--><%- ctx.getPrettyName(tag.names[0]) %> <!--
|
||||
--><%- ctx.getPrettyName(tag.names[0]) %><!--
|
||||
--><% if (ctx.canListPosts) { %><!--
|
||||
--></a><!--
|
||||
--><% } %><!--
|
||||
--><% } %> <!--
|
||||
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
||||
--></li><!--
|
||||
--><% } %><!--
|
||||
|
@ -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>
|
||||
<% } %>
|
||||
|
@ -16,7 +16,6 @@
|
||||
%><form class='horizontal bulk-edit bulk-edit-tags'><%
|
||||
%><span class='append hint'>Tagging with:</span><%
|
||||
%><a href class='mousetrap button append open'>Mass tag</a><%
|
||||
%><wbr/><%
|
||||
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
|
||||
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
|
||||
%><a href class='mousetrap button append close'>Stop tagging</a><%
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
<%= item.operation %>
|
||||
|
||||
<%= ctx.makeResourceLink(item.type, item.id) %>
|
||||
<%= ctx.makeResourceLink(item.type, item.id, item.data) %>
|
||||
</div>
|
||||
|
||||
<div class='details'><!--
|
||||
|
@ -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><!--
|
||||
|
@ -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>
|
||||
|
@ -398,7 +398,7 @@ class Api extends events.EventTarget {
|
||||
|
||||
if (data) {
|
||||
if (files && Object.keys(files).length) {
|
||||
req.attach("metadata", new Blob([JSON.stringify(data)]));
|
||||
req.attach("metadata", new Blob([JSON.stringify(data)], { type: "application/json" }));
|
||||
} else {
|
||||
req.set("Content-Type", "application/json");
|
||||
req.send(data);
|
||||
|
@ -91,16 +91,16 @@ class PoolController {
|
||||
_evtUpdate(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
if (e.detail.names !== undefined) {
|
||||
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||
e.detail.pool.names = e.detail.names;
|
||||
}
|
||||
if (e.detail.category !== undefined) {
|
||||
if (e.detail.category !== undefined && e.detail.category !== null) {
|
||||
e.detail.pool.category = e.detail.category;
|
||||
}
|
||||
if (e.detail.description !== undefined) {
|
||||
if (e.detail.description !== undefined && e.detail.description !== null) {
|
||||
e.detail.pool.description = e.detail.description;
|
||||
}
|
||||
if (e.detail.posts !== undefined) {
|
||||
if (e.detail.posts !== undefined && e.detail.posts !== null) {
|
||||
e.detail.pool.posts.clear();
|
||||
for (let postId of e.detail.posts) {
|
||||
e.detail.pool.posts.add(
|
||||
|
@ -43,6 +43,8 @@ class PoolListController {
|
||||
this._headerView.addEventListener(
|
||||
"submit",
|
||||
(e) => this._evtSubmit(e),
|
||||
);
|
||||
this._headerView.addEventListener(
|
||||
"navigate",
|
||||
(e) => this._evtNavigate(e)
|
||||
);
|
||||
|
@ -169,22 +169,22 @@ class PostMainController extends BasePostController {
|
||||
this._view.sidebarControl.disableForm();
|
||||
this._view.sidebarControl.clearMessages();
|
||||
const post = e.detail.post;
|
||||
if (e.detail.safety !== undefined) {
|
||||
if (e.detail.safety !== undefined && e.detail.safety !== null) {
|
||||
post.safety = e.detail.safety;
|
||||
}
|
||||
if (e.detail.flags !== undefined) {
|
||||
if (e.detail.flags !== undefined && e.detail.flags !== null) {
|
||||
post.flags = e.detail.flags;
|
||||
}
|
||||
if (e.detail.relations !== undefined) {
|
||||
if (e.detail.relations !== undefined && e.detail.relations !== null) {
|
||||
post.relations = e.detail.relations;
|
||||
}
|
||||
if (e.detail.content !== undefined) {
|
||||
if (e.detail.content !== undefined && e.detail.content !== null) {
|
||||
post.newContent = e.detail.content;
|
||||
}
|
||||
if (e.detail.thumbnail !== undefined) {
|
||||
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) {
|
||||
post.newThumbnail = e.detail.thumbnail;
|
||||
}
|
||||
if (e.detail.source !== undefined) {
|
||||
if (e.detail.source !== undefined && e.detail.source !== null) {
|
||||
post.source = e.detail.source;
|
||||
}
|
||||
post.save().then(
|
||||
@ -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
|
||||
|
@ -95,13 +95,13 @@ class TagController {
|
||||
_evtUpdate(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
if (e.detail.names !== undefined) {
|
||||
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||
e.detail.tag.names = e.detail.names;
|
||||
}
|
||||
if (e.detail.category !== undefined) {
|
||||
if (e.detail.category !== undefined && e.detail.category !== null) {
|
||||
e.detail.tag.category = e.detail.category;
|
||||
}
|
||||
if (e.detail.description !== undefined) {
|
||||
if (e.detail.description !== undefined && e.detail.description !== null) {
|
||||
e.detail.tag.description = e.detail.description;
|
||||
}
|
||||
e.detail.tag.save().then(
|
||||
|
@ -175,21 +175,21 @@ class UserController {
|
||||
const isLoggedIn = api.isLoggedIn(e.detail.user);
|
||||
const infix = isLoggedIn ? "self" : "any";
|
||||
|
||||
if (e.detail.name !== undefined) {
|
||||
if (e.detail.name !== undefined && e.detail.name !== null) {
|
||||
e.detail.user.name = e.detail.name;
|
||||
}
|
||||
if (e.detail.email !== undefined) {
|
||||
if (e.detail.email !== undefined && e.detail.email !== null) {
|
||||
e.detail.user.email = e.detail.email;
|
||||
}
|
||||
if (e.detail.rank !== undefined) {
|
||||
if (e.detail.rank !== undefined && e.detail.rank !== null) {
|
||||
e.detail.user.rank = e.detail.rank;
|
||||
}
|
||||
|
||||
if (e.detail.password !== undefined) {
|
||||
if (e.detail.password !== undefined && e.detail.password !== null) {
|
||||
e.detail.user.password = e.detail.password;
|
||||
}
|
||||
|
||||
if (e.detail.avatarStyle !== undefined) {
|
||||
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) {
|
||||
e.detail.user.avatarStyle = e.detail.avatarStyle;
|
||||
if (e.detail.avatarContent) {
|
||||
e.detail.user.avatarContent = e.detail.avatarContent;
|
||||
@ -302,7 +302,7 @@ class UserController {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
|
||||
if (e.detail.note !== undefined) {
|
||||
if (e.detail.note !== undefined && e.detail.note !== null) {
|
||||
e.detail.userToken.note = e.detail.note;
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,12 @@ class FileDropperControl extends events.EventTarget {
|
||||
this._urlInputNode.addEventListener("keydown", (e) =>
|
||||
this._evtUrlInputKeyDown(e)
|
||||
);
|
||||
this._urlInputNode.addEventListener("paste", (e) => {
|
||||
// document.onpaste is used on the post-upload page.
|
||||
// And this event is used on the post edit page.
|
||||
if (document.getElementById("post-upload")) return;
|
||||
this._evtPaste(e)
|
||||
});
|
||||
}
|
||||
if (this._urlConfirmButtonNode) {
|
||||
this._urlConfirmButtonNode.addEventListener("click", (e) =>
|
||||
@ -55,6 +61,11 @@ class FileDropperControl extends events.EventTarget {
|
||||
);
|
||||
}
|
||||
|
||||
document.onpaste = (e) => {
|
||||
if (!document.getElementById("post-upload")) return;
|
||||
this._evtPaste(e)
|
||||
}
|
||||
|
||||
this._originalHtml = this._dropperNode.innerHTML;
|
||||
views.replaceContent(target, source);
|
||||
}
|
||||
@ -129,6 +140,17 @@ class FileDropperControl extends events.EventTarget {
|
||||
this._emitFiles(e.dataTransfer.files);
|
||||
}
|
||||
|
||||
_evtPaste(e) {
|
||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||
const fileList = Array.from(items).map((x) => x.getAsFile()).filter(f => f);
|
||||
|
||||
if (!this._options.allowMultiple && fileList.length > 1) {
|
||||
window.alert("Cannot select multiple files.");
|
||||
} else if (fileList.length > 0) {
|
||||
this._emitFiles(fileList);
|
||||
}
|
||||
}
|
||||
|
||||
_evtUrlInputKeyDown(e) {
|
||||
if (e.which !== KEY_RETURN) {
|
||||
return;
|
||||
|
@ -103,6 +103,30 @@ class PostContentControl {
|
||||
}
|
||||
|
||||
_refreshSize() {
|
||||
if (window.innerWidth <= 800) {
|
||||
const buttons = document.querySelector(".sidebar > .buttons");
|
||||
if (buttons) {
|
||||
const content = document.querySelector(".content");
|
||||
content.insertBefore(buttons, content.querySelector(".post-container + *"));
|
||||
|
||||
const afterControls = document.querySelector(".content > .after-mobile-controls");
|
||||
if (afterControls) {
|
||||
afterControls.parentElement.parentElement.appendChild(afterControls);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const buttons = document.querySelector(".content > .buttons");
|
||||
if (buttons) {
|
||||
const sidebar = document.querySelector(".sidebar");
|
||||
sidebar.insertBefore(buttons, sidebar.firstElementChild);
|
||||
}
|
||||
|
||||
const afterControls = document.querySelector(".content + .after-mobile-controls");
|
||||
if (afterControls) {
|
||||
document.querySelector(".content").appendChild(afterControls);
|
||||
}
|
||||
}
|
||||
|
||||
this._currentFitFunction();
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
@ -427,7 +440,7 @@ class PostEditSidebarControl extends events.EventTarget {
|
||||
: undefined,
|
||||
|
||||
thumbnail:
|
||||
this._newPostThumbnail !== undefined
|
||||
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
|
||||
? this._newPostThumbnail
|
||||
: undefined,
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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() {
|
||||
@ -271,7 +271,7 @@ class Post extends events.EventTarget {
|
||||
if (this._newContent) {
|
||||
files.content = this._newContent;
|
||||
}
|
||||
if (this._newThumbnail !== undefined) {
|
||||
if (this._newThumbnail !== undefined && this._newThumbnail !== null) {
|
||||
files.thumbnail = this._newThumbnail;
|
||||
}
|
||||
if (this._source !== this._orig._source) {
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
@ -209,13 +209,13 @@ function makePostLink(id, includeHash) {
|
||||
}
|
||||
|
||||
function makeTagLink(name, includeHash, includeCount, tag) {
|
||||
const category = tag ? tag.category : "unknown";
|
||||
const category = tag && tag.category ? tag.category : "unknown";
|
||||
let text = misc.getPrettyName(name);
|
||||
if (includeHash === true) {
|
||||
text = "#" + text;
|
||||
}
|
||||
if (includeCount === true) {
|
||||
text += " (" + (tag ? tag.postCount : 0) + ")";
|
||||
text += " (" + (tag && tag.postCount ? tag.postCount : 0) + ")";
|
||||
}
|
||||
return api.hasPrivilege("tags:view")
|
||||
? makeElement(
|
||||
@ -234,15 +234,15 @@ function makeTagLink(name, includeHash, includeCount, tag) {
|
||||
}
|
||||
|
||||
function makePoolLink(id, includeHash, includeCount, pool, name) {
|
||||
const category = pool ? pool.category : "unknown";
|
||||
const category = pool && pool.category ? pool.category : "unknown";
|
||||
let text = misc.getPrettyName(
|
||||
name ? name : pool ? pool.names[0] : "unknown"
|
||||
name ? name : pool && pool.names ? pool.names[0] : "pool " + id
|
||||
);
|
||||
if (includeHash === true) {
|
||||
text = "#" + text;
|
||||
}
|
||||
if (includeCount === true) {
|
||||
text += " (" + (pool ? pool.postCount : 0) + ")";
|
||||
text += " (" + (pool && pool.postCount ? pool.postCount : 0) + ")";
|
||||
}
|
||||
return api.hasPrivilege("pools:view")
|
||||
? makeElement(
|
||||
@ -261,10 +261,10 @@ 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 && api.hasPrivilege("users:view")
|
||||
user && user.name && api.hasPrivilege("users:view")
|
||||
? makeElement(
|
||||
"a",
|
||||
{ href: uri.formatClientLink("user", user.name) },
|
||||
@ -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]) {
|
||||
|
@ -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: {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
12
doc/API.md
12
doc/API.md
@ -54,7 +54,7 @@
|
||||
- [Deleting pool category](#deleting-pool-category)
|
||||
- [Setting default pool category](#setting-default-pool-category)
|
||||
- Pools
|
||||
- [Listing pools](#listing-pool)
|
||||
- [Listing pools](#listing-pools)
|
||||
- [Creating pool](#creating-pool)
|
||||
- [Updating pool](#updating-pool)
|
||||
- [Getting pool](#getting-pool)
|
||||
@ -165,9 +165,9 @@ way. The files, however, should be passed as regular fields appended with a
|
||||
accepts a file named `content`, the client should pass
|
||||
`{"contentUrl":"http://example.com/file.jpg"}` as a part of the JSON message
|
||||
body. When creating or updating post content using this method, the server can
|
||||
also be configured to employ [youtube-dl](https://github.com/ytdl-org/youtube-dl)
|
||||
to download content from popular sites such as youtube, gfycat, etc. Access to
|
||||
youtube-dl can be configured with the `'uploads:use_downloader'` permission
|
||||
also be configured to employ [yt-dlp](https://github.com/yt-dlp/yt-dlp) to
|
||||
download content from popular sites such as youtube, gfycat, etc. Access to
|
||||
yt-dlp can be configured with the `'uploads:use_downloader'` permission
|
||||
|
||||
Finally, in some cases the user might want to reuse one file between the
|
||||
requests to save the bandwidth (for example, reverse search + consecutive
|
||||
@ -789,7 +789,7 @@ data.
|
||||
| `fav-time` | alias of `fav-date` |
|
||||
| `feature-date` | featured at given date |
|
||||
| `feature-time` | alias of `feature-time` |
|
||||
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. |
|
||||
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` or `unsafe`. |
|
||||
| `rating` | alias of `safety` |
|
||||
|
||||
**Sort style tokens**
|
||||
@ -1389,7 +1389,7 @@ data.
|
||||
## Creating pool
|
||||
- **Request**
|
||||
|
||||
`POST /pools/create`
|
||||
`POST /pool`
|
||||
|
||||
- **Input**
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
This assumes that you have Docker (version 17.05 or greater)
|
||||
and Docker Compose (version 1.6.0 or greater) already installed.
|
||||
This assumes that you have Docker (version 19.03 or greater)
|
||||
and the Docker Compose CLI (version 1.27.0 or greater) already installed.
|
||||
|
||||
### Prepare things
|
||||
|
||||
@ -38,7 +38,7 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
||||
|
||||
This pulls the latest containers from docker.io:
|
||||
```console
|
||||
user@host:szuru$ docker-compose pull
|
||||
user@host:szuru$ docker compose pull
|
||||
```
|
||||
|
||||
If you have modified the application's source and would like to manually
|
||||
@ -49,17 +49,17 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
||||
|
||||
For first run, it is recommended to start the database separately:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d sql
|
||||
user@host:szuru$ docker compose up -d sql
|
||||
```
|
||||
|
||||
To start all containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d
|
||||
user@host:szuru$ docker compose up -d
|
||||
```
|
||||
|
||||
To view/monitor the application logs:
|
||||
```console
|
||||
user@host:szuru$ docker-compose logs -f
|
||||
user@host:szuru$ docker compose logs -f
|
||||
# (CTRL+C to exit)
|
||||
```
|
||||
|
||||
@ -84,13 +84,13 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
||||
2. Build the containers:
|
||||
|
||||
```console
|
||||
user@host:szuru$ docker-compose build
|
||||
user@host:szuru$ docker compose build
|
||||
```
|
||||
|
||||
That will attempt to build both containers, but you can specify `client`
|
||||
or `server` to make it build only one.
|
||||
|
||||
If `docker-compose build` spits out:
|
||||
If `docker compose build` spits out:
|
||||
|
||||
```
|
||||
ERROR: Service 'server' failed to build: failed to parse platform : "" is an invalid component of "": platform specifier component must match "^[A-Za-z0-9_-]+$": invalid argument
|
||||
@ -102,7 +102,7 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
||||
user@host:szuru$ export DOCKER_BUILDKIT=1; export COMPOSE_DOCKER_CLI_BUILD=1
|
||||
```
|
||||
|
||||
...and run `docker-compose build` again.
|
||||
...and run `docker compose build` again.
|
||||
|
||||
*Note: If your changes are not taking effect in your builds, consider building
|
||||
with `--no-cache`.*
|
||||
@ -117,7 +117,7 @@ with `--no-cache`.*
|
||||
run from docker:
|
||||
|
||||
```console
|
||||
user@host:szuru$ docker-compose run server ./szuru-admin --help
|
||||
user@host:szuru$ docker compose run server ./szuru-admin --help
|
||||
```
|
||||
|
||||
will give you a breakdown on all available commands.
|
||||
|
@ -1,9 +1,7 @@
|
||||
## Example Docker Compose configuration
|
||||
##
|
||||
## Use this as a template to set up docker-compose, or as guide to set up other
|
||||
## Use this as a template to set up docker compose, or as guide to set up other
|
||||
## orchestration services
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
server:
|
||||
|
@ -23,15 +23,15 @@ RUN apk --no-cache add \
|
||||
py3-pillow \
|
||||
py3-pynacl \
|
||||
py3-tz \
|
||||
py3-pyrfc3339 \
|
||||
&& pip3 install --no-cache-dir --disable-pip-version-check \
|
||||
py3-pyrfc3339
|
||||
RUN pip3 install --no-cache-dir --disable-pip-version-check \
|
||||
"alembic>=0.8.5" \
|
||||
"coloredlogs==5.0" \
|
||||
"pyheif==0.6.1" \
|
||||
"heif-image-plugin>=0.3.2" \
|
||||
youtube_dl \
|
||||
"pillow-avif-plugin>=1.1.0" \
|
||||
&& apk --no-cache del py3-pip
|
||||
yt-dlp \
|
||||
"pillow-avif-plugin~=1.1.0"
|
||||
RUN apk --no-cache del py3-pip
|
||||
|
||||
COPY ./ /opt/app/
|
||||
RUN rm -rf /opt/app/szurubooru/tests
|
||||
|
@ -3,7 +3,7 @@ certifi>=2017.11.5
|
||||
coloredlogs==5.0
|
||||
heif-image-plugin==0.3.2
|
||||
numpy>=1.8.2
|
||||
pillow-avif-plugin>=1.1.0
|
||||
pillow-avif-plugin~=1.1.0
|
||||
pillow>=4.3.0
|
||||
psycopg2-binary>=2.6.1
|
||||
pyheif==0.6.1
|
||||
@ -12,4 +12,4 @@ pyRFC3339>=1.0
|
||||
pytz>=2018.3
|
||||
pyyaml>=3.11
|
||||
SQLAlchemy>=1.0.12, <1.4
|
||||
youtube_dl
|
||||
yt-dlp
|
||||
|
@ -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"]
|
||||
)
|
||||
|
@ -21,7 +21,7 @@ def _merge(left: Dict, right: Dict) -> Dict:
|
||||
return left
|
||||
|
||||
|
||||
def _docker_config() -> Dict:
|
||||
def _container_config() -> Dict:
|
||||
if "TEST_ENVIRONMENT" not in os.environ:
|
||||
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
|
||||
if key not in os.environ:
|
||||
@ -49,6 +49,15 @@ def _file_config(filename: str) -> Dict:
|
||||
return yaml.load(handle.read(), Loader=yaml.SafeLoader) or {}
|
||||
|
||||
|
||||
def _running_inside_container() -> bool:
|
||||
env = os.environ.keys()
|
||||
return (
|
||||
os.path.exists("/.dockerenv")
|
||||
or "KUBERNETES_SERVICE_HOST" in env
|
||||
or "container" in env # set by lxc/podman
|
||||
)
|
||||
|
||||
|
||||
def _read_config() -> Dict:
|
||||
ret = _file_config("config.yaml.dist")
|
||||
if os.path.isfile("config.yaml"):
|
||||
@ -57,8 +66,8 @@ def _read_config() -> Dict:
|
||||
logger.warning(
|
||||
"'config.yaml' should be a file, not a directory, skipping"
|
||||
)
|
||||
if os.path.exists("/.dockerenv"):
|
||||
ret = _merge(ret, _docker_config())
|
||||
if _running_inside_container():
|
||||
ret = _merge(ret, _container_config())
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -64,7 +64,7 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
|
||||
|
||||
|
||||
def _get_youtube_dl_content_url(url: str) -> str:
|
||||
cmd = ["youtube-dl", "--format", "best", "--no-playlist"]
|
||||
cmd = ["yt-dlp", "--format", "best", "--no-playlist"]
|
||||
if config.config["user_agent"]:
|
||||
cmd.extend(["--user-agent", config.config["user_agent"]])
|
||||
cmd.extend(["--get-url", url])
|
||||
|
@ -531,8 +531,10 @@ def generate_alternate_formats(
|
||||
def get_default_flags(content: bytes) -> List[str]:
|
||||
assert content
|
||||
ret = []
|
||||
if mime.is_video(mime.get_mime_type(content)):
|
||||
ret.append(model.Post.FLAG_LOOP)
|
||||
if mime.is_animated_gif(content):
|
||||
if images.check_for_loop(content):
|
||||
ret.append(model.Post.FLAG_LOOP)
|
||||
elif mime.is_video(mime.get_mime_type(content)):
|
||||
if images.Image(content).check_for_sound():
|
||||
ret.append(model.Post.FLAG_SOUND)
|
||||
return ret
|
||||
|
@ -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)
|
||||
|
||||
|
Reference in New Issue
Block a user