summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--client/css/comment-control.styl4
-rw-r--r--client/css/core-general.styl4
-rw-r--r--client/css/post-list-view.styl3
-rw-r--r--client/css/post-main-view.styl49
-rw-r--r--client/css/user-list-view.styl3
-rw-r--r--client/html/index.htm2
-rw-r--r--client/html/post_main.tpl28
-rw-r--r--client/html/post_readonly_sidebar.tpl8
-rw-r--r--client/html/posts_header.tpl1
-rw-r--r--client/js/controllers/pool_list_controller.js2
-rw-r--r--client/js/controls/post_content_control.js24
-rw-r--r--client/js/util/views.js12
-rw-r--r--doc/API.md14
-rw-r--r--server/Dockerfile10
-rw-r--r--server/requirements.txt4
-rw-r--r--server/szurubooru/func/net.py2
-rw-r--r--server/szurubooru/search/configs/post_search_config.py29
-rw-r--r--server/szurubooru/tests/search/configs/test_post_search_config.py52
19 files changed, 186 insertions, 67 deletions
diff --git a/README.md b/README.md
index 9307055..6a38501 100644
--- a/README.md
+++ b/README.md
@@ -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))
diff --git a/client/css/comment-control.styl b/client/css/comment-control.styl
index 0d5959b..21bbf72 100644
--- a/client/css/comment-control.styl
+++ b/client/css/comment-control.styl
@@ -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-all
+
ul, ol
list-style-position: inside
margin: 1em 0
diff --git a/client/css/core-general.styl b/client/css/core-general.styl
index 7e5883a..d25c5f6 100644
--- a/client/css/core-general.styl
+++ b/client/css/core-general.styl
@@ -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
diff --git a/client/css/post-list-view.styl b/client/css/post-list-view.styl
index 7f6aa80..e4f75d5 100644
--- a/client/css/post-list-view.styl
+++ b/client/css/post-list-view.styl
@@ -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
diff --git a/client/css/post-main-view.styl b/client/css/post-main-view.styl
index e643dfb..1183cce 100644
--- a/client/css/post-main-view.styl
+++ b/client/css/post-main-view.styl
@@ -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
diff --git a/client/css/user-list-view.styl b/client/css/user-list-view.styl
index f1569f8..64915b4 100644
--- a/client/css/user-list-view.styl
+++ b/client/css/user-list-view.styl
@@ -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
diff --git a/client/html/index.htm b/client/html/index.htm
index 0072890..2f0f4e4 100644
--- a/client/html/index.htm
+++ b/client/html/index.htm
@@ -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'/>
diff --git a/client/html/post_main.tpl b/client/html/post_main.tpl
index 54c5733..84e48b1 100644
--- a/client/html/post_main.tpl
+++ b/client/html/post_main.tpl
@@ -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,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>
diff --git a/client/html/post_readonly_sidebar.tpl b/client/html/post_readonly_sidebar.tpl
index 0f93ae3..60efc26 100644
--- a/client/html/post_readonly_sidebar.tpl
+++ b/client/html/post_readonly_sidebar.tpl
@@ -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><% } %><!--
@@ -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><!--
--><% } %><!--
diff --git a/client/html/posts_header.tpl b/client/html/posts_header.tpl
index d1422d2..77d16ed 100644
--- a/client/html/posts_header.tpl
+++ b/client/html/posts_header.tpl
@@ -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><%
diff --git a/client/js/controllers/pool_list_controller.js b/client/js/controllers/pool_list_controller.js
index 91d655c..a66f816 100644
--- a/client/js/controllers/pool_list_controller.js
+++ b/client/js/controllers/pool_list_controller.js
@@ -43,6 +43,8 @@ class PoolListController {
this._headerView.addEventListener(
"submit",
(e) => this._evtSubmit(e),
+ );
+ this._headerView.addEventListener(
"navigate",
(e) => this._evtNavigate(e)
);
diff --git a/client/js/controls/post_content_control.js b/client/js/controls/post_content_control.js
index 55daca7..8cbd5d8 100644
--- a/client/js/controls/post_content_control.js
+++ b/client/js/controls/post_content_control.js
@@ -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();
}
diff --git a/client/js/util/views.js b/client/js/util/views.js
index 38c98a1..f6280a1 100644
--- a/client/js/util/views.js
+++ b/client/js/util/views.js
@@ -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(
@@ -264,7 +264,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) },
diff --git a/doc/API.md b/doc/API.md
index 63a50a2..00ee75a 100644
--- a/doc/API.md
+++ b/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
@@ -323,7 +323,7 @@ data.
{
"name": <name>,
"color": <color>,
- "order": <order> // optional
+ "order": <order>
}
```
@@ -1389,7 +1389,7 @@ data.
## Creating pool
- **Request**
- `POST /pools/create`
+ `POST /pool`
- **Input**
@@ -2491,7 +2491,7 @@ One file together with its metadata posted to the site.
## Micro post
**Description**
-A [post resource](#post) stripped down to `name` and `thumbnailUrl` fields.
+A [post resource](#post) stripped down to `id` and `thumbnailUrl` fields.
## Note
**Description**
diff --git a/server/Dockerfile b/server/Dockerfile
index 487f192..3e4dadf 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -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
diff --git a/server/requirements.txt b/server/requirements.txt
index 16b29ff..ffe18f0 100644
--- a/server/requirements.txt
+++ b/server/requirements.txt
@@ -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
diff --git a/server/szurubooru/func/net.py b/server/szurubooru/func/net.py
index c53a62e..d6aa95e 100644
--- a/server/szurubooru/func/net.py
+++ b/server/szurubooru/func/net.py
@@ -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])
diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py
index ddc003b..8d4672d 100644
--- a/server/szurubooru/search/configs/post_search_config.py
+++ b/server/szurubooru/search/configs/post_search_config.py
@@ -122,6 +122,34 @@ def _pool_filter(
)(query, criterion, negated)
+def _category_filter(
+ query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool
+) -> SaQuery:
+ assert criterion
+
+ # Step 1. find the id for the category
+ q1 = db.session.query(model.TagCategory.tag_category_id).filter(
+ model.TagCategory.name == criterion.value
+ )
+
+ # Step 2. find the tags with that category
+ q2 = db.session.query(model.Tag.tag_id).filter(
+ model.Tag.category_id.in_(q1)
+ )
+
+ # Step 3. find all posts that have at least one of those tags
+ q3 = db.session.query(model.PostTag.post_id).filter(
+ model.PostTag.tag_id.in_(q2)
+ )
+
+ # Step 4. profit
+ expr = model.Post.post_id.in_(q3)
+ if negated:
+ expr = ~expr
+
+ return query.filter(expr)
+
+
class PostSearchConfig(BaseSearchConfig):
def __init__(self) -> None:
self.user = None # type: Optional[model.User]
@@ -349,6 +377,7 @@ class PostSearchConfig(BaseSearchConfig):
),
),
(["pool"], _pool_filter),
+ (["category"], _category_filter),
]
)
diff --git a/server/szurubooru/tests/search/configs/test_post_search_config.py b/server/szurubooru/tests/search/configs/test_post_search_config.py
index 4fb8191..b86fa27 100644
--- a/server/szurubooru/tests/search/configs/test_post_search_config.py
+++ b/server/szurubooru/tests/search/configs/test_post_search_config.py
@@ -863,3 +863,55 @@ def test_tumbleweed(
db.session.flush()
verify_unpaged("special:tumbleweed", [4])
verify_unpaged("-special:tumbleweed", [1, 2, 3])
+
+
+@pytest.mark.parametrize(
+ "input,expected_post_ids",
+ [
+ ("category:cat1", [1, 2, 3]),
+ ("category:cat2", [3, 4]),
+ ],
+)
+def test_search_by_tag_category(
+ verify_unpaged,
+ post_factory,
+ tag_factory,
+ tag_category_factory,
+ input,
+ expected_post_ids,
+):
+ cat1 = tag_category_factory(name="cat1")
+ cat2 = tag_category_factory(name="cat2")
+ tag1 = tag_factory(names=["t1"], category=cat1)
+ tag2 = tag_factory(names=["t2"], category=cat1)
+ tag3 = tag_factory(names=["t3"], category=cat2)
+
+ post1 = post_factory(id=1)
+ post1.tags.append(tag1)
+
+ post2 = post_factory(id=2)
+ post2.tags.append(tag2)
+
+ post3 = post_factory(id=3)
+ post3.tags.append(tag1)
+ post3.tags.append(tag3)
+
+ post4 = post_factory(id=4)
+ post4.tags.append(tag3)
+
+ post5 = post_factory(id=5)
+
+ db.session.add_all(
+ [
+ tag1,
+ tag2,
+ tag3,
+ post1,
+ post2,
+ post3,
+ post4,
+ post5,
+ ]
+ )
+ db.session.flush()
+ verify_unpaged(input, expected_post_ids)