74 Commits

Author SHA1 Message Date
Eva
6438e67ed6 Merge aafcfc33bb into 376f687c38 2025-04-03 01:55:11 +00:00
Eva
aafcfc33bb client/pools: prioritize loading of first thumbnail 2025-04-03 03:55:05 +02:00
Eva
d7ffdb0997 client/css: remove semicolons 2025-04-03 02:48:46 +02:00
Eva
0971745615 server/tests: fix post serialization tests 2025-04-03 02:26:46 +02:00
Eva
27f94be56d server/tests: fix nearby pool posts tests 2025-04-03 02:01:30 +02:00
Eva
98227e562e server/tests: fix sort:pool test indentation 2025-04-03 02:01:30 +02:00
Eva
5244718e0f client/css: pool thumbnail outline
Same as posts in search results.
2025-04-03 02:01:30 +02:00
Eva
ac64c1ca50 client/css: add missing variable for pool categories page 2025-04-03 02:01:30 +02:00
Eva
ff788a5e30 client/pools: use cheaper pool post listing for unprivileged users 2025-04-03 02:01:30 +02:00
Eva
0acc522bfc server/pools: add field for retrieving only the first 3 posts 2025-04-03 02:01:30 +02:00
Eva
a5cf49a94a client/pools: remove broken selector 2025-04-03 02:01:30 +02:00
Eva
374f4a5fa9 client/posts: split query by any whitespace 2025-04-03 02:01:30 +02:00
Eva
3875ec173f client, server: merge nearby pool posts into regular post serialization
Can still be cleaned up some more.
Need to compare speed of the get_around query vs nearby pool posts.
2025-04-03 02:01:30 +02:00
Eva
7708b4e5a3 client/pools: fix empty pool thumbnail display 2025-04-01 09:50:03 +02:00
Eva
64b7b6d0bc server/posts: optimize nearby pool posts
This was very slow when any entry was unavailable, such as on
single-post pools, or edges of pools (first/last post).
Also only fetch id. Previously it would get the thumbnail url.
2025-04-01 08:27:03 +02:00
Eva
0601c32598 client/css: fix pool thumbnail animations and outline on firefox 2025-04-01 08:25:50 +02:00
Eva
4792f01362 client/posts: use correct pool's posts when overriding navigation
We were always using the first pool the current post belongs to.
2025-04-01 08:24:58 +02:00
Eva
cf0a64d832 client/css: pool navigator styling 2025-04-01 08:24:48 +02:00
Eva
2e0dd251b2 client/posts: remove unavailable first and last links in pool navigator 2025-04-01 08:24:44 +02:00
Eva
0ff359d613 client/posts: replace main navigation with pool navigation when in pool 2025-04-01 08:24:36 +02:00
Eva
769b4f0e22 client/pools: sort by negated pool order by default 2025-04-01 08:24:24 +02:00
Eva
7823682b39 server/search: allow negating sort:pool 2025-04-01 08:22:41 +02:00
Eva
5034121be6 client/help: document sort:pool 2025-04-01 08:22:36 +02:00
2ff79a6aa2 server/search: support sorting post search results by pool post order 2025-04-01 08:22:16 +02:00
Eva
ef48b07966 client/pools: expand animation 2025-04-01 08:22:16 +02:00
Eva
1507e6bf2b client/pools: thumbnail hover animation and thinner focus outline
Update stylus for :has support
2025-04-01 08:22:16 +02:00
Eva
d59eac948b client/pool_navigator: respect 'display underscores as spaces' setting 2025-04-01 08:22:16 +02:00
Eva
745186062d client/css: use zoomed-in thumbnails for pools 2025-04-01 08:22:16 +02:00
63dbff36a0 client/pools: stacked thumbnail appearance for pool list page 2025-04-01 08:22:16 +02:00
c3705f6ee2 client/pools: thumbnail view in pool list 2025-04-01 08:22:15 +02:00
871ebe4083 server/tests: add necessary privilege to fixture 2025-04-01 08:12:18 +02:00
650d9784c0 server/tests: add some tests 2025-04-01 08:12:07 +02:00
4f663293c0 doc: properly name API elements 2025-04-01 08:11:57 +02:00
86320fe227 client: fix some incorrect references 2025-04-01 08:07:38 +02:00
ae899853d2 client: append missing child 2025-04-01 08:07:38 +02:00
e4f7e9e8e0 client: add missing import 2025-04-01 08:07:37 +02:00
7c12abeffa client: add pool navigation elements
this implementation was *heavily* cherry-picked from PR #403.
2025-04-01 08:07:37 +02:00
c3a4cb6cd1 server: add none check 2025-04-01 08:04:04 +02:00
75138525e8 server: izip doesnt exist anymore 2025-04-01 08:04:04 +02:00
f8dfde9a61 server: better iterable logic 2025-04-01 08:04:04 +02:00
7586d92db5 server: slightly better way of prev/next 2025-04-01 08:04:04 +02:00
10be19050d server: fix incorrect values being used 2025-04-01 08:04:04 +02:00
377998fdbc server: add missing None argument 2025-04-01 08:04:04 +02:00
9b2e1c3064 server: rename incorrect flag 2025-04-01 08:04:04 +02:00
b30db8caf8 server: fix small typo 2025-04-01 08:04:04 +02:00
c0504692e1 server: poolpost nearby implementation 2025-04-01 08:04:04 +02:00
376f687c38 chore: questionable is not a recognized rating 2025-02-11 21:50:27 +01:00
4fd848abf2 doc: use docker compose instead of docker-compose
The minimum version requirements are rough guesses, in practice any decently modern docker installation should work.
2025-02-11 21:25:10 +01:00
61b9f81e39 Fixed the google search option in the post details view 2024-11-17 16:48:24 +01:00
b721865931 server/config: generalize container support
Allow running in Kubernetes, podman, and LXC, besides plain docker-compose,
without having to fake out /.dockerenv in non-Docker environments.
2024-11-10 15:44:39 +01:00
Neo
46e3295003 Upload from clipboard (#414)
client/upload: upload from clipboard

Co-authored-by: Eva <evauwu@riseup.net>
2024-09-29 14:54:53 +02:00
031131506e client/css: fix comment word-break
`break-all` makes it hard to read actual comments.
2024-09-29 13:48:06 +02:00
Neo
d102578b54 Merge pull request #647 from po5/null-checks
client: add null checks
2024-04-27 21:23:16 +02:00
Neo
6edb25d87b Merge pull request #641 from po5/mobile
Mobile improvements
2024-04-26 22:56:58 +02:00
Neo
93fc15f2a4 Merge pull request #642 from po5/better-links 2024-04-26 22:37:54 +02:00
Neo
4f9d46e1c2 Merge branch 'master' into better-links 2024-04-26 22:16:37 +02:00
Eva
b72e81850d client: add null checks 2024-03-28 13:31:48 +01:00
Eva
c1c695f082 client/css: stack bulk tagging toggles horizontally on mobile 2024-03-21 22:26:49 +01:00
Eva
4b6b231fc8 client/posts: reorder elements in mobile layout
Navigation is always right below the image, and comments are always
at the very bottom, to minimize scrolling for common actions.
2024-03-21 22:26:28 +01:00
Eva
6b0c3cfc7f client/html: allow mobile browsers to zoom in 2024-03-21 22:23:45 +01:00
Eva
4ec8cb3ba2 client/css: constrain thumbnails to parent to prevent overextended links 2024-03-21 22:19:46 +01:00
Eva
8d971234a2 client/views: better pool name fallback 2024-03-21 22:16:05 +01:00
Eva
a16bb198ab client/views: more thorough link fallbacks
Prevents a bunch of errors that can happen when a resource is deleted.
2024-03-21 21:53:11 +01:00
Eva
3f182a66ad client/posts: fix overextended tag link 2024-03-21 21:52:52 +01:00
Eva
b52363e82d client/posts: fix overextended download link 2024-03-21 21:52:49 +01:00
Eva
3bf45e4c0a client/users: fix overextended avatar links 2024-03-21 21:52:39 +01:00
5596f53744 posts page ugly horizontal bar fix
fixes ugly horizontal scrollbar appearing when a post with extremely wide image is present in the posts list
2024-02-29 20:56:27 +01:00
da425afc49 Pin pillow-avif-plugin to compatible version range 2024-02-21 17:47:27 +01:00
d7394d672f Fix Pool Search 2024-02-21 01:27:00 +01:00
190d795426 doc: fix small error in pool API docs 2023-12-05 21:31:23 +01:00
7c92ceaf6a fix overflow on comments, prevents ugly unnecesary horizontal scroll 2023-11-05 12:27:03 +01:00
Neo
9e00f37464 Merge pull request #597 from zakame/use-yt-dlp
server/net: use yt-dlp instead of youtube-dl
2023-11-05 12:22:03 +01:00
59c497e168 doc: update for yt-dlp 2023-08-17 20:58:09 +08:00
c292b96f06 server/net: use yt-dlp instead of youtube-dl
youtube-dl no longer even gets URLs properly, so switch to yt-dlp as a
drop-in replacement for it.
2023-08-17 20:41:50 +08:00
54 changed files with 856 additions and 434 deletions

View File

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

View File

@ -36,6 +36,7 @@ $button-disabled-background-color = #CCC
$post-thumbnail-border-color = $main-color
$post-thumbnail-no-tags-border-color = #F44
$default-tag-category-background-color = $active-tab-background-color
$default-pool-category-background-color = $active-tab-background-color
$new-tag-background-color = #DFC
$new-tag-text-color = black
$implied-tag-background-color = #FFC

View File

@ -127,6 +127,10 @@ $comment-border-color = #DDD
color: mix($main-color, $inactive-link-color-darktheme)
.comment-content
p
word-wrap: normal
word-break: break-word
ul, ol
list-style-position: inside
margin: 1em 0

View File

@ -300,10 +300,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

View File

@ -1,47 +1,100 @@
@import colors
.pool-list
table
width: 100%
border-spacing: 0
text-align: left
line-height: 1.3em
tr:hover td
background: $top-navigation-color
th, td
padding: 0.1em 0.5em
th
white-space: nowrap
background: $top-navigation-color
.names
width: 84%
.post-count
text-align: center
width: 8%
.creation-time
text-align: center
width: 8%
white-space: pre
ul
list-style-type: none
margin: 0
padding: 0
display: inline
li
padding: 0
display: inline
&:not(:last-child):after
content: ', '
@media (max-width: 800px)
.posts
display: none
ul
list-style-type: none
padding: 0
display: flex
align-content: flex-end
flex-wrap: wrap
margin: 0 -0.25em
.darktheme .pool-list
table
tr:hover td
background: $top-navigation-color-darktheme
th
background: $top-navigation-color-darktheme
li
position: relative
flex-grow: 1
margin: 2em 1.5em 2em 1.2em
display: inline-block
text-align: left
min-width: 10em
width: 12vw
&:not(.flexbox-dummy)
min-height: 7.5em
height: 9vw
.thumbnail-wrapper
display: inline-block
width: 100%
height: 100%
line-height: 80%
font-size: 80%
color: white
outline: none
border-right: 20px solid transparent
&:before
content: ' '
display: block
position: relative
width: 100%
height: 20px
bottom: 20px
.thumbnail
width: 100%
height: 100%
outline-offset: -2px
background-size: cover
transition: top .1s ease-in-out, right .1s ease-in-out
background-position: 50% 30%
position: absolute
display: inline-block
box-shadow: 0 0 0 1px rgba(0,0,0,0.2)
.thumbnail-1, .thumbnail.empty
right: -4px
top: -4px
z-index: 30
.thumbnail-2
right: -10px
top: -10px
z-index: 20
.thumbnail-3
right: -16px
top: -16px
z-index: 10
.pool-name
color: black
font-size: 1em
text-align: center
a
width: 100%
display: inline-block
a:active, a:focus
.thumbnail
outline: 2px solid $main-color !important
.pool-list ul li:hover
.thumbnail-wrapper
.thumbnail-1
right: -0px
top: -0px
.thumbnail-3
right: -20px
top: -20px
.pool-list ul li:has(a:focus), .pool-list ul li:has(a:active)
.thumbnail-wrapper
.thumbnail-1
right: -0px
top: -0px
.thumbnail-3
right: -20px
top: -20px
.pool-list-header
label
@ -61,3 +114,21 @@
.darktheme .pool-list-header
.append
color: $inactive-link-color-darktheme
.post-flow
ul
li
min-width: inherit
width: inherit
margin: 0 0.25em 0.5em 0.25em
&:not(.flexbox-dummy)
height: 14vw
.thumbnail
position: static
outline-offset: -1px
.thumbnail-wrapper.no-tags
.thumbnail
outline: 2px solid $post-thumbnail-no-tags-border-color
&:hover a, a:active, a:focus
.thumbnail
outline: 2px solid $main-color !important

View File

@ -0,0 +1,39 @@
@import colors
.pool-navigator-container
padding: 0
margin: 0 auto
.pool-info-wrapper
box-sizing: border-box
width: 100%
margin: 0 0 1em 0
display: flex
padding: 0.5em 1em
border: 1px solid $line-color
background: $top-navigation-color
.pool-name
flex: 1 1
text-align: center
overflow: hidden
white-space: nowrap
-o-text-overflow: ellipsis
text-overflow: ellipsis
.first, .last
flex-basis: 1em
.first, .prev, .next, .last
flex: 0 1
white-space: nowrap
>span
padding-top: 2px
padding-bottom: 2px
margin: 0 .25em
.darktheme .pool-navigator-container .pool-info-wrapper
border: 1px solid $top-navigation-color-darktheme
background: $window-color-darktheme

View File

@ -0,0 +1,9 @@
.pool-navigators>ul
list-style-type: none
margin: 0
padding: 0
>li
margin-bottom: 1em
&:last-child
margin-bottom: 0

View File

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

View File

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

View File

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

View File

@ -329,6 +329,10 @@
<td><code>feature-time</code></td>
<td>alias of <code>feature-time</code></td>
</tr>
<tr>
<td><code>pool</code></td>
<td>pool order, requires pool named token</code></td>
</tr>
</tbody>
</table>

View File

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

View File

@ -1,6 +1,6 @@
<div class='pool-delete'>
<form>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id + ' -sort:pool'}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
<ul class='input'>
<li>

View File

@ -0,0 +1,49 @@
<div class='pool-navigator-container'>
<div class='pool-info-wrapper'>
<span class='first'>
<% if (ctx.canViewPosts && ctx.previousPost && ctx.firstPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.firstPost.id, ctx.parameters) %>'>
<% } %>
«
<% if (ctx.canViewPosts && ctx.previousPost && ctx.firstPost) { %>
</a>
<% } %>
</span>
<span class='prev'>
<% if (ctx.canViewPosts && ctx.previousPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.previousPost.id, ctx.parameters) %>'>
<% } %>
prev
<% if (ctx.canViewPosts && ctx.previousPost) { %>
</a>
<% } %>
</span>
<span class='pool-name'>
<% if (ctx.canViewPools) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.formatClientLink("pool", ctx.pool.id) %>'>
<% } %>
Pool: <%- ctx.getPrettyName(ctx.pool.names[0]) %>
<% if (ctx.canViewPools) { %>
</a>
<% } %>
</span>
<span class='next'>
<% if (ctx.canViewPosts && ctx.nextPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.nextPost.id, ctx.parameters) %>'>
<% } %>
next
<% if (ctx.canViewPosts && ctx.nextPost) { %>
</a>
<% } %>
</span>
<span class='last'>
<% if (ctx.canViewPosts && ctx.nextPost && ctx.lastPost) { %>
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.lastPost.id, ctx.parameters) %>'>
<% } %>
»
<% if (ctx.canViewPosts && ctx.nextPost && ctx.lastPost) { %>
</a>
<% } %>
</span>
</div>
</div>

View File

@ -0,0 +1,4 @@
<div class='pool-navigators'>
<ul>
</ul>
</div>

View File

@ -18,6 +18,6 @@
<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>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id + ' -sort:pool'}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
</section>
</div>

View File

@ -1,48 +1,19 @@
<div class='pool-list table-wrap'>
<% if (ctx.postFlow) { %><div class='pool-list post-flow'><% } else { %><div class='pool-list'><% } %>
<% if (ctx.response.results.length) { %>
<table>
<thead>
<th class='names'>
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:name'}) %>'>Pool name(s)</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:name'}) %>'>Pool name(s)</a>
<ul>
<% for (let pool of ctx.response.results) { %>
<li data-pool-id='<%= pool.id %>'>
<a class='thumbnail-wrapper' href='<%= ctx.canViewPools ? ctx.formatClientLink("pool", pool.id) : "" %>'>
<% if (ctx.canViewPosts) { %>
<%= ctx.makePoolThumbnails(pool.posts, ctx.postFlow) %>
<% } %>
</th>
<th class='post-count'>
<% if (ctx.parameters.query == 'sort:post-count') { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:post-count'}) %>'>Post count</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:post-count'}) %>'>Post count</a>
<% } %>
</th>
<th class='creation-time'>
<% if (ctx.parameters.query == 'sort:creation-time') { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:creation-time'}) %>'>Created on</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:creation-time'}) %>'>Created on</a>
<% } %>
</th>
</thead>
<tbody>
<% for (let pool of ctx.response.results) { %>
<tr>
<td class='names'>
<ul>
<% for (let name of pool.names) { %>
<li><%= ctx.makePoolLink(pool.id, false, false, pool, name) %></li>
<% } %>
</ul>
</td>
<td class='post-count'>
<a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + pool.id}) %>'><%- pool.postCount %></a>
</td>
<td class='creation-time'>
<%= ctx.makeRelativeTime(pool.creationTime) %>
</td>
</tr>
<% } %>
</tbody>
</table>
</a>
<div class='pool-name'>
<%= ctx.makePoolLink(pool.id, false, false, pool, name) %>
</div>
</li>
<% } %>
<%= ctx.makeFlexboxAlign() %>
</ul>
<% } %>
</div>

View File

@ -29,6 +29,7 @@
<span class='vim-nav-hint'>Next post &gt;</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,19 @@
<div class='content'>
<div class='post-container'></div>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% if (ctx.canListPools && ctx.canViewPools) { %>
<div class='pool-navigators-container'></div>
<% } %>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<div class='after-mobile-controls'>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
</div>
</div>
</div>

View File

@ -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> &middot;
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot;
<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]) %>&#32;<!--
--><%- ctx.getPrettyName(tag.names[0]) %><!--
--><% if (ctx.canListPosts) { %><!--
--></a><!--
--><% } %><!--
--><% } %>&#32;<!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!--
--><% } %><!--

View File

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

View File

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

View File

@ -2,6 +2,7 @@
const router = require("../router.js");
const api = require("../api.js");
const settings = require("../models/settings.js");
const uri = require("../util/uri.js");
const PoolList = require("../models/pool_list.js");
const topNavigation = require("../models/top_navigation.js");
@ -13,7 +14,6 @@ const EmptyView = require("../views/empty_view.js");
const fields = [
"id",
"names",
"posts",
"creationTime",
"postCount",
"category",
@ -43,6 +43,8 @@ class PoolListController {
this._headerView.addEventListener(
"submit",
(e) => this._evtSubmit(e),
);
this._headerView.addEventListener(
"navigate",
(e) => this._evtNavigate(e)
);
@ -98,14 +100,21 @@ class PoolListController {
return uri.formatClientLink("pools", parameters);
},
requestPage: (offset, limit) => {
const canEditPosts = api.hasPrivilege("pools:edit") || api.hasPrivilege("pools:edit:posts");
const effectiveFields = fields.concat([canEditPosts ? "posts": "postsMicro"]);
return PoolList.search(
this._ctx.parameters.query,
offset,
limit,
fields
effectiveFields
);
},
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege("posts:view"),
canViewPools: api.hasPrivilege("pools:view"),
postFlow: settings.get().postFlow,
});
return new PoolsPageView(pageCtx);
},
});

View File

@ -11,6 +11,7 @@ const PostList = require("../models/post_list.js");
const PostMainView = require("../views/post_main_view.js");
const BasePostController = require("./base_post_controller.js");
const EmptyView = require("../views/empty_view.js");
const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js");
class PostMainController extends BasePostController {
constructor(ctx, editMode) {
@ -26,6 +27,7 @@ class PostMainController extends BasePostController {
]).then(
(responses) => {
const [post, aroundResponse] = responses;
let aroundPool = null;
// remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh
@ -39,23 +41,36 @@ class PostMainController extends BasePostController {
)
: uri.formatClientLink("post", ctx.parameters.id);
router.replace(url, ctx.state, false);
misc.splitByWhitespace(parameters.query).forEach((item) => {
const found = item.match(/^pool:([0-9]+)/i);
if (found) {
const activePool = parseInt(found[1]);
post.pools.map((pool) => {
if (pool.id == activePool) {
aroundPool = pool;
}
});
}
});
}
this._post = post;
this._view = new PostMainView({
post: post,
editMode: editMode,
prevPostId: aroundResponse.prev
? aroundResponse.prev.id
: null,
nextPostId: aroundResponse.next
? aroundResponse.next.id
: null,
prevPostId: aroundPool
? (aroundPool.previousPost ? aroundPool.previousPost.id : null)
: (aroundResponse.prev ? aroundResponse.prev.id : null),
nextPostId: aroundPool
? (aroundPool.nextPost ? aroundPool.nextPost.id : null)
: (aroundResponse.next ? aroundResponse.next.id : null),
canEditPosts: api.hasPrivilege("posts:edit"),
canDeletePosts: api.hasPrivilege("posts:delete"),
canFeaturePosts: api.hasPrivilege("posts:feature"),
canListComments: api.hasPrivilege("comments:list"),
canCreateComments: api.hasPrivilege("comments:create"),
canListPools: api.hasPrivilege("pools:list"),
canViewPools: api.hasPrivilege("pools:view"),
parameters: parameters,
});
@ -169,22 +184,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(

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
"use strict";
const api = require("../api.js");
const misc = require("../util/misc.js");
const events = require("../events.js");
const views = require("../util/views.js");
const template = views.getTemplate("pool-navigator");
class PoolNavigatorControl extends events.EventTarget {
constructor(hostNode, poolPostNearby) {
super();
this._hostNode = hostNode;
this._poolPostNearby = poolPostNearby;
views.replaceContent(
this._hostNode,
template({
pool: poolPostNearby,
parameters: { query: `pool:${poolPostNearby.id}` },
linkClass: misc.makeCssName(poolPostNearby.category, "pool"),
canViewPosts: api.hasPrivilege("posts:view"),
canViewPools: api.hasPrivilege("pools:view"),
firstPost: poolPostNearby.firstPost,
previousPost: poolPostNearby.previousPost,
nextPost: poolPostNearby.nextPost,
lastPost: poolPostNearby.lastPost,
getPrettyName: misc.getPrettyName,
})
);
}
}
module.exports = PoolNavigatorControl;

View File

@ -0,0 +1,49 @@
"use strict";
const events = require("../events.js");
const views = require("../util/views.js");
const PoolNavigatorControl = require("../controls/pool_navigator_control.js");
const template = views.getTemplate("pool-navigator-list");
class PoolNavigatorListControl extends events.EventTarget {
constructor(hostNode, poolPostNearby) {
super();
this._hostNode = hostNode;
this._poolPostNearby = poolPostNearby;
this._indexToNode = {};
for (const entry of this._poolPostNearby) {
this._installPoolNavigatorNode(entry);
}
}
get _poolNavigatorListNode() {
return this._hostNode;
}
_installPoolNavigatorNode(poolPostNearby) {
const poolListItemNode = document.createElement("div");
const poolControl = new PoolNavigatorControl(
poolListItemNode,
poolPostNearby,
);
this._indexToNode[poolPostNearby.id] = poolListItemNode;
this._poolNavigatorListNode.appendChild(poolListItemNode);
}
_uninstallPoolNavigatorNode(index) {
const poolListItemNode = this._indexToNode[index];
poolListItemNode.parentNode.removeChild(poolListItemNode);
}
_evtAdd(e) {
this._installPoolNavigatorNode(e.detail.index);
}
_evtRemove(e) {
this._uninstallPoolNavigatorNode(e.detail.index);
}
}
module.exports = PoolNavigatorListControl;

View File

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

View File

@ -427,7 +427,7 @@ class PostEditSidebarControl extends events.EventTarget {
: undefined,
thumbnail:
this._newPostThumbnail !== undefined
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
? this._newPostThumbnail
: undefined,

View File

@ -36,7 +36,7 @@ class Pool extends events.EventTarget {
}
get posts() {
return this._posts;
return this._postsMicro || this._posts;
}
get postCount() {
@ -51,6 +51,22 @@ class Pool extends events.EventTarget {
return this._lastEditTime;
}
get firstPost() {
return this._firstPost;
}
get lastPost() {
return this._lastPost;
}
get previousPost() {
return this._previousPost;
}
get nextPost() {
return this._nextPost;
}
set names(value) {
this._names = value;
}
@ -169,10 +185,15 @@ class Pool extends events.EventTarget {
_creationTime: response.creationTime,
_lastEditTime: response.lastEditTime,
_postCount: response.postCount || 0,
_postsMicro: response.postsMicro,
_firstPost: response.firstPost || null,
_lastPost: response.lastPost || null,
_previousPost: response.previousPost || null,
_nextPost: response.nextPost || null,
};
for (let obj of [this, this._orig]) {
obj._posts.sync(response.posts);
obj._posts.sync(response.posts || []);
}
Object.assign(this, map);

View File

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

View File

@ -40,19 +40,36 @@ function makeRelativeTime(time) {
);
}
function makeThumbnail(url) {
function makeThumbnail(url, klass, extraProperties) {
return makeElement(
"span",
url
? {
class: "thumbnail",
class: klass || "thumbnail",
style: `background-image: url(\'${url}\')`,
}
: { class: "thumbnail empty" },
makeElement("img", { alt: "thumbnail", src: url })
makeElement("img", Object.assign({ alt: "thumbnail", src: url }, extraProperties || {}))
);
}
function makePoolThumbnails(posts, postFlow) {
if (posts.length == 0) {
return makeThumbnail(null);
}
if (postFlow) {
return makeThumbnail(posts.at(0).thumbnailUrl);
}
let s = "";
for (let i = 0; i < Math.min(3, posts.length); i++) {
s += makeThumbnail(posts.at(i).thumbnailUrl, "thumbnail thumbnail-" + (i+1), i === 0 ? {fetchPriority: "high"} : {});
}
return s;
}
function makeRadio(options) {
_imbueId(options);
return makeElement(
@ -209,13 +226,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 +251,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(
@ -254,7 +271,7 @@ function makePoolLink(id, includeHash, includeCount, pool, name) {
misc.escapeHtml(text)
)
: makeElement(
"span",
"div",
{ class: misc.makeCssName(category, "pool") },
misc.escapeHtml(text)
);
@ -264,7 +281,7 @@ function makeUserLink(user) {
let text = makeThumbnail(user ? user.avatarUrl : null);
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) },
@ -436,6 +453,7 @@ function getTemplate(templatePath) {
makeFileSize: makeFileSize,
makeMarkdown: makeMarkdown,
makeThumbnail: makeThumbnail,
makePoolThumbnails: makePoolThumbnails,
makeRadio: makeRadio,
makeCheckbox: makeCheckbox,
makeSelect: makeSelect,

View File

@ -30,7 +30,7 @@ class PoolCreateView extends events.EventTarget {
}
for (let node of this._formNode.querySelectorAll(
"input, select, textarea, posts"
"input, select, textarea"
)) {
node.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));

View File

@ -10,6 +10,7 @@ const PostContentControl = require("../controls/post_content_control.js");
const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js");
const PostReadonlySidebarControl = require("../controls/post_readonly_sidebar_control.js");
const PostEditSidebarControl = require("../controls/post_edit_sidebar_control.js");
const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js");
const CommentControl = require("../controls/comment_control.js");
const CommentListControl = require("../controls/comment_list_control.js");
@ -57,6 +58,7 @@ class PostMainView {
this._installSidebar(ctx);
this._installCommentForm();
this._installComments(ctx.post.comments);
this._installPoolNavigators(ctx);
const showPreviousImage = () => {
if (ctx.prevPostId) {
@ -137,6 +139,20 @@ class PostMainView {
}
}
_installPoolNavigators(ctx) {
const poolNavigatorsContainerNode = document.querySelector(
"#content-holder .pool-navigators-container"
);
if (!poolNavigatorsContainerNode) {
return;
}
this.poolNavigatorsControl = new PoolNavigatorListControl(
poolNavigatorsContainerNode,
ctx.post.pools,
);
}
_installCommentForm() {
const commentFormContainer = document.querySelector(
"#content-holder .comment-form-container"

264
client/package-lock.json generated
View File

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

View File

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

View File

@ -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**
@ -975,6 +975,43 @@ data.
Retrieves information about posts that are before or after an existing post.
## Getting pools around post
- **Request**
`GET /post/<id>/pools-nearby`
- **Output**
```json5
[
{
"pool": <pool>,
"firstPost": <first-post>,
"lastPost": <last-post>,
"nextPost": <next-post>,
"previousPost": <previous-post>
},
...
]
```
- **Field meaning**
- `<pool>`: The associated [micro pool resource](#micro-pool).
- `<first-post>`: A [micro post resource](#micro-post) that displays the first post in the pool.
- `<last-post>`: A [micro post resource](#micro-post) that displays the last post in the pool.
- `<next-post>`: A [micro post resource](#micro-post) that displays the next post in the pool.
- `<previous-post>`: A [micro post resource](#micro-post) that displays the previous post in the pool.
- **Errors**
- the post does not exist
- privileges are too low
- **Description**
Retrieves extra information about any pools that the post is in.
## Deleting post
- **Request**
@ -1389,7 +1426,7 @@ data.
## Creating pool
- **Request**
`POST /pools/create`
`POST /pool`
- **Input**

View File

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

View File

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

View File

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

View File

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

View File

@ -5,12 +5,10 @@ from szurubooru import db, errors, model, rest, search
from szurubooru.func import (
auth,
favorites,
mime,
posts,
scores,
serialization,
snapshots,
tags,
versions,
)

View File

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

View File

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

View File

@ -108,6 +108,7 @@ class PoolSerializer(serialization.BaseSerializer):
"lastEditTime": self.serialize_last_edit_time,
"postCount": self.serialize_post_count,
"posts": self.serialize_posts,
"postsMicro": self.serialize_posts_micro,
}
def serialize_id(self) -> Any:
@ -143,6 +144,14 @@ class PoolSerializer(serialization.BaseSerializer):
]
]
def serialize_posts_micro(self) -> Any:
posts_micro = []
for i, rel in enumerate(self.pool.posts):
posts_micro.append(posts.serialize_micro_post(rel, None))
if i == 2:
break
return posts_micro
def serialize_pool(
pool: model.Pool, options: List[str] = []

View File

@ -1,12 +1,15 @@
import hmac
import logging
from collections import namedtuple
from datetime import datetime
from itertools import tee, chain, islice
from typing import Any, Callable, Dict, List, Optional, Tuple
import sqlalchemy as sa
from szurubooru import config, db, errors, model, rest
from szurubooru.func import (
auth,
comments,
files,
image_hash,
@ -15,7 +18,6 @@ from szurubooru.func import (
pools,
scores,
serialization,
snapshots,
tags,
users,
util,
@ -96,6 +98,13 @@ FLAG_MAP = {
model.Post.FLAG_SOUND: "sound",
}
# https://stackoverflow.com/a/1012089
def _get_nearby_iter(post_list):
previous_item, current_item, next_item = tee(post_list, 3)
previous_item = chain([None], previous_item)
next_item = chain(islice(next_item, 1, None), [None])
return zip(previous_item, current_item, next_item)
def get_post_security_hash(id: int) -> str:
return hmac.new(
@ -337,8 +346,10 @@ class PostSerializer(serialization.BaseSerializer):
]
def serialize_pools(self) -> List[Any]:
if not auth.has_privilege(self.auth_user, "pools:list"):
return []
return [
pools.serialize_micro_pool(pool)
{**pools.serialize_micro_pool(pool), **get_pool_posts_nearby(self.post, pool)} if auth.has_privilege(self.auth_user, "pools:view") else pools.serialize_micro_pool(pool)
for pool in sorted(
self.post.pools, key=lambda pool: pool.creation_time
)
@ -968,3 +979,38 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]:
]
else:
return []
def serialize_safe_post(
post: Optional[model.Post]
) -> rest.Response:
return {"id": getattr(post, "post_id", None)} if post else None
def serialize_id_post(
post_id: Optional[int]
) -> rest.Response:
return serialize_safe_post(try_get_post_by_id(post_id)) if post_id else None
def get_pool_posts_nearby(
post: model.Post, pool: model.Pool
) -> rest.Response:
prev_post_id = None
next_post_id = None
first_post_id = pool.posts[0].post_id,
last_post_id = pool.posts[-1].post_id,
for previous_item, current_item, next_item in _get_nearby_iter(pool.posts):
if post.post_id == current_item.post_id:
if previous_item != None:
prev_post_id = previous_item.post_id
if next_item != None:
next_post_id = next_item.post_id
break
return {
"firstPost": serialize_id_post(first_post_id),
"lastPost": serialize_id_post(last_post_id),
"previousPost": serialize_id_post(prev_post_id),
"nextPost": serialize_id_post(next_post_id),
}

View File

@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, Tuple
from typing import Any, Dict, Optional, Tuple, Callable, Union
import sqlalchemy as sa
@ -114,12 +114,26 @@ def _pool_filter(
query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool
) -> SaQuery:
assert criterion
return search_util.create_subquery_filter(
model.Post.post_id,
model.PoolPost.post_id,
model.PoolPost.pool_id,
search_util.create_num_filter,
)(query, criterion, negated)
from szurubooru.search.configs import util as search_util
subquery = db.session.query(model.PoolPost.post_id.label("foreign_id"))
subquery = subquery.options(sa.orm.lazyload("*"))
subquery = search_util.create_num_filter(model.PoolPost.pool_id)(subquery, criterion, False)
subquery = subquery.subquery("t")
expression = model.Post.post_id.in_(subquery)
if negated:
expression = ~expression
return query.filter(expression)
def _pool_sort(
query: SaQuery, pool_id: Optional[int], order: str
) -> SaQuery:
if pool_id is None:
return query
db_query = query.join(model.PoolPost, sa.and_(model.PoolPost.post_id == model.Post.post_id, model.PoolPost.pool_id == pool_id))
if order == tokens.SortToken.SORT_DESC:
return db_query.order_by(model.PoolPost.order.desc())
return db_query.order_by(model.PoolPost.order.asc())
def _category_filter(
@ -153,6 +167,7 @@ def _category_filter(
class PostSearchConfig(BaseSearchConfig):
def __init__(self) -> None:
self.user = None # type: Optional[model.User]
self.pool_id = None # type: Optional[int]
def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery:
new_special_tokens = []
@ -177,6 +192,10 @@ class PostSearchConfig(BaseSearchConfig):
else:
new_special_tokens.append(token)
search_query.special_tokens = new_special_tokens
self.pool_id = None
for token in search_query.named_tokens:
if token.name == "pool" and isinstance(token.criterion, criteria.PlainCriterion):
self.pool_id = token.criterion.value
def create_around_query(self) -> SaQuery:
return db.session.query(model.Post).options(sa.orm.lazyload("*"))
@ -382,7 +401,7 @@ class PostSearchConfig(BaseSearchConfig):
)
@property
def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]:
def sort_columns(self) -> Dict[str, Union[Tuple[SaColumn, str], Callable[[SaQuery], None]]]:
return util.unalias_dict(
[
(
@ -444,6 +463,10 @@ class PostSearchConfig(BaseSearchConfig):
["feature-date", "feature-time"],
(model.Post.last_feature_time, self.SORT_DESC),
),
(
["pool"],
lambda subquery, order: _pool_sort(subquery, self.pool_id, order)
)
]
)

View File

@ -205,6 +205,7 @@ def create_subquery_filter(
filter_column: SaColumn,
filter_factory: SaColumn,
subquery_decorator: Callable[[SaQuery], None] = None,
order: SaQuery = None,
) -> Filter:
filter_func = filter_factory(filter_column)

View File

@ -181,14 +181,19 @@ class Executor:
_format_dict_keys(self.config.sort_columns),
)
)
column, default_order = self.config.sort_columns[
entry = self.config.sort_columns[
sort_token.name
]
order = _get_order(sort_token.order, default_order)
if order == sort_token.SORT_ASC:
db_query = db_query.order_by(column.asc())
elif order == sort_token.SORT_DESC:
db_query = db_query.order_by(column.desc())
if callable(entry):
order = _get_order(sort_token.order, sort_token.SORT_DESC)
db_query = entry(db_query, order)
else:
column, default_order = entry
order = _get_order(sort_token.order, default_order)
if order == sort_token.SORT_ASC:
db_query = db_query.order_by(column.asc())
elif order == sort_token.SORT_DESC:
db_query = db_query.order_by(column.desc())
db_query = self.config.finalize_query(db_query)
return db_query

View File

@ -1,4 +1,4 @@
from typing import Any, Callable
from typing import Any, Callable, Union
SaColumn = Any
SaQuery = Any

View File

@ -14,6 +14,7 @@ def inject_config(config_injector):
"privileges": {
"posts:list": model.User.RANK_REGULAR,
"posts:view": model.User.RANK_REGULAR,
"pools:list": model.User.RANK_REGULAR,
},
}
)
@ -125,3 +126,25 @@ def test_trying_to_retrieve_single_without_privileges(
context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)),
{"post_id": 999},
)
def test_get_pool_post_around(user_factory, post_factory, pool_factory, pool_post_factory):
p1 = post_factory(id=1)
p2 = post_factory(id=2)
p3 = post_factory(id=3)
db.session.add_all([p1, p2, p3])
pool = pool_factory(id=1)
db.session.add(pool)
pool_posts = [pool_post_factory(pool=pool, post=p1), pool_post_factory(pool=pool, post=p2), pool_post_factory(pool=pool, post=p3)]
db.session.add_all(pool_posts)
result = posts.get_pool_posts_nearby(p1, pool)
assert result["previousPost"] == None and result["nextPost"]["id"] == 2
result = posts.get_pool_posts_nearby(p2, pool)
assert result["previousPost"]["id"] == 1 and result["nextPost"]["id"] == 3
result = posts.get_pool_posts_nearby(p3, pool)
assert result["previousPost"]["id"] == 2 and result["nextPost"] == None

View File

@ -105,7 +105,14 @@ def test_serialize_post(
pool_category_factory,
config_injector,
):
config_injector({"data_url": "http://example.com/", "secret": "test"})
config_injector({
"privileges": {
"pools:list": model.User.RANK_REGULAR,
"pools:view": model.User.RANK_REGULAR,
},
"data_url": "http://example.com/",
"secret": "test"
})
with patch("szurubooru.func.comments.serialize_comment"), patch(
"szurubooru.func.users.serialize_micro_user"
), patch("szurubooru.func.posts.files.has"):
@ -249,6 +256,10 @@ def test_serialize_post(
"description": "desc",
"category": "test-cat1",
"postCount": 1,
"firstPost": {"id": 1},
"lastPost": {"id": 1},
"previousPost": None,
"nextPost": None,
},
{
"id": 2,
@ -256,6 +267,10 @@ def test_serialize_post(
"description": "desc2",
"category": "test-cat2",
"postCount": 1,
"firstPost": {"id": 1},
"lastPost": {"id": 1},
"previousPost": None,
"nextPost": None,
},
],
"user": "post author",

View File

@ -725,6 +725,7 @@ def test_filter_by_feature_date(
"sort:fav-time",
"sort:feature-date",
"sort:feature-time",
"sort:pool",
],
)
def test_sort_tokens(verify_unpaged, post_factory, input):
@ -865,6 +866,45 @@ def test_tumbleweed(
verify_unpaged("-special:tumbleweed", [1, 2, 3])
def test_sort_pool(
post_factory, pool_factory, pool_category_factory, verify_unpaged
):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post4 = post_factory(id=4)
pool1 = pool_factory(
id=1,
names=["pool1"],
description="desc",
category=pool_category_factory("test-cat1"),
)
pool1.posts = [post1, post4, post3]
pool2 = pool_factory(
id=2,
names=["pool2"],
description="desc",
category=pool_category_factory("test-cat2"),
)
pool2.posts = [post3, post4, post2]
db.session.add_all(
[
post1,
post2,
post3,
post4,
pool1,
pool2
]
)
db.session.flush()
verify_unpaged("pool:1 sort:pool", [1, 4, 3])
verify_unpaged("pool:2 sort:pool", [3, 4, 2])
verify_unpaged("pool:1 pool:2 sort:pool", [4, 3])
verify_unpaged("pool:2 pool:1 sort:pool", [3, 4])
verify_unpaged("sort:pool", [1, 2, 3, 4])
@pytest.mark.parametrize(
"input,expected_post_ids",
[